From 39bd173e4146b9221c7a84665cca77cc6c3bceba Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Fri, 31 Jan 2020 10:45:27 -0500 Subject: [PATCH 01/33] Upgrade node-sass to 4.13.1 (#56484) --- package.json | 2 +- packages/kbn-plugin-helpers/package.json | 2 +- packages/kbn-ui-framework/package.json | 2 +- yarn.lock | 28 +++++++++++------------- 4 files changed, 16 insertions(+), 18 deletions(-) diff --git a/package.json b/package.json index 425527a058e862..e249546e715812 100644 --- a/package.json +++ b/package.json @@ -454,7 +454,7 @@ "murmurhash3js": "3.0.1", "mutation-observer": "^1.0.3", "nock": "10.0.6", - "node-sass": "^4.9.4", + "node-sass": "^4.13.1", "normalize-path": "^3.0.0", "nyc": "^14.1.1", "pixelmatch": "^5.1.0", diff --git a/packages/kbn-plugin-helpers/package.json b/packages/kbn-plugin-helpers/package.json index 68af0aa791c8e0..3b358c03b8053a 100644 --- a/packages/kbn-plugin-helpers/package.json +++ b/packages/kbn-plugin-helpers/package.json @@ -24,7 +24,7 @@ "gulp-zip": "5.0.1", "inquirer": "^1.2.2", "minimatch": "^3.0.4", - "node-sass": "^4.9.4", + "node-sass": "^4.13.1", "through2": "^2.0.3", "through2-map": "^3.0.0", "vinyl-fs": "^3.0.3" diff --git a/packages/kbn-ui-framework/package.json b/packages/kbn-ui-framework/package.json index b4d9d3dfee03f6..4bb4c660a01aba 100644 --- a/packages/kbn-ui-framework/package.json +++ b/packages/kbn-ui-framework/package.json @@ -53,7 +53,7 @@ "jquery": "^3.4.1", "keymirror": "0.1.1", "moment": "^2.24.0", - "node-sass": "^4.9.4", + "node-sass": "^4.13.1", "postcss": "^7.0.5", "postcss-loader": "^3.0.0", "raw-loader": "^3.1.0", diff --git a/yarn.lock b/yarn.lock index 3563ee3fc2733f..a3acc2ae216c5a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -19371,7 +19371,7 @@ lodash.clone@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.clone/-/lodash.clone-4.5.0.tgz#195870450f5a13192478df4bc3d23d2dea1907b6" integrity sha1-GVhwRQ9aExkkeN9Lw9I9LeoZB7Y= -lodash.clonedeep@^4.3.2, lodash.clonedeep@^4.5.0: +lodash.clonedeep@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8= @@ -19516,11 +19516,6 @@ lodash.merge@^4.4.0, lodash.merge@^4.6.1: resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== -lodash.mergewith@^4.6.0: - version "4.6.2" - resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz#617121f89ac55f59047c7aec1ccd6654c6590f55" - integrity sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ== - lodash.omit@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.omit/-/lodash.omit-4.5.0.tgz#6eb19ae5a1ee1dd9df0b969e66ce0b7fa30b5e60" @@ -20947,7 +20942,12 @@ mute-stream@0.0.8: resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== -nan@^2.10.0, nan@^2.9.2: +nan@^2.13.2: + version "2.14.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c" + integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg== + +nan@^2.9.2: version "2.10.0" resolved "https://registry.yarnpkg.com/nan/-/nan-2.10.0.tgz#96d0cd610ebd58d4b4de9cc0c6828cda99c7548f" integrity sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA== @@ -21350,10 +21350,10 @@ node-releases@^1.1.25: dependencies: semver "^5.3.0" -node-sass@^4.9.4: - version "4.9.4" - resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.9.4.tgz#349bd7f1c89422ffe7e1e4b60f2055a69fbc5512" - integrity sha512-MXyurANsUoE4/6KmfMkwGcBzAnJQ5xJBGW7Ei6ea8KnUKuzHr/SguVBIi3uaUAHtZCPUYkvlJ3Ef5T5VAwVpaA== +node-sass@^4.13.1: + version "4.13.1" + resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.13.1.tgz#9db5689696bb2eec2c32b98bfea4c7a2e992d0a3" + integrity sha512-TTWFx+ZhyDx1Biiez2nB0L3YrCZ/8oHagaDalbuBSlqXgUPsdkUSzJsVxeDO9LtPB49+Fh3WQl3slABo6AotNw== dependencies: async-foreach "^0.1.3" chalk "^1.1.1" @@ -21362,12 +21362,10 @@ node-sass@^4.9.4: get-stdin "^4.0.1" glob "^7.0.3" in-publish "^2.0.0" - lodash.assign "^4.2.0" - lodash.clonedeep "^4.3.2" - lodash.mergewith "^4.6.0" + lodash "^4.17.15" meow "^3.7.0" mkdirp "^0.5.1" - nan "^2.10.0" + nan "^2.13.2" node-gyp "^3.8.0" npmlog "^4.0.0" request "^2.88.0" From 5ed7be9498dda3b6723347e88e34ba6a63341bf3 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Fri, 31 Jan 2020 17:03:15 +0100 Subject: [PATCH 02/33] Remove unused dependencies in dashboard (#56318) --- .../kibana/public/dashboard/legacy_imports.ts | 7 --- .../public/dashboard/np_ready/application.ts | 49 ++++--------------- .../public/dashboard/np_ready/legacy_app.js | 13 ++--- 3 files changed, 13 insertions(+), 56 deletions(-) diff --git a/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts b/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts index 97d165b6b5c23e..14bdef99ca8948 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts @@ -38,15 +38,8 @@ export { showSaveModal, SaveResult } from 'ui/saved_objects/show_saved_object_sa export { migrateLegacyQuery } from 'ui/utils/migrate_legacy_query'; export { KbnUrl } from 'ui/url/kbn_url'; // @ts-ignore -export { GlobalStateProvider } from 'ui/state_management/global_state'; -// @ts-ignore -export { StateManagementConfigProvider } from 'ui/state_management/config_provider'; -// @ts-ignore export { PrivateProvider } from 'ui/private/private'; // @ts-ignore -export { EventsProvider } from 'ui/events'; -export { PersistedState } from 'ui/persisted_state'; -// @ts-ignore export { createTopNavDirective, createTopNavHelper } from 'ui/kbn_top_nav/kbn_top_nav'; // @ts-ignore export { PromiseServiceCreator } from 'ui/promises/promises'; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/application.ts b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/application.ts index 1a0a99311d06b0..0d461028d994a3 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/application.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/application.ts @@ -33,15 +33,12 @@ import { confirmModalFactory, createTopNavDirective, createTopNavHelper, - EventsProvider, IPrivate, KbnUrlProvider, - PersistedState, PrivateProvider, PromiseServiceCreator, RedirectWhenMissingProvider, SavedObjectLoader, - StateManagementConfigProvider, } from '../legacy_imports'; // @ts-ignore import { initDashboardApp } from './legacy_app'; @@ -112,8 +109,6 @@ function createLocalAngularModule(core: AppMountContext['core'], navigation: Nav createLocalPromiseModule(); createLocalConfigModule(core); createLocalKbnUrlModule(); - createLocalStateModule(); - createLocalPersistedStateModule(); createLocalTopNavModule(navigation); createLocalConfirmModalModule(); createLocalIconModule(); @@ -123,9 +118,9 @@ function createLocalAngularModule(core: AppMountContext['core'], navigation: Nav 'app/dashboard/Config', 'app/dashboard/I18n', 'app/dashboard/Private', - 'app/dashboard/PersistedState', 'app/dashboard/TopNav', - 'app/dashboard/State', + 'app/dashboard/KbnUrl', + 'app/dashboard/Promise', 'app/dashboard/ConfirmModal', 'app/dashboard/icon', ]); @@ -145,29 +140,6 @@ function createLocalConfirmModalModule() { .directive('confirmModal', reactDirective => reactDirective(EuiConfirmModal)); } -function createLocalStateModule() { - angular.module('app/dashboard/State', [ - 'app/dashboard/Private', - 'app/dashboard/Config', - 'app/dashboard/KbnUrl', - 'app/dashboard/Promise', - 'app/dashboard/PersistedState', - ]); -} - -function createLocalPersistedStateModule() { - angular - .module('app/dashboard/PersistedState', ['app/dashboard/Private', 'app/dashboard/Promise']) - .factory('PersistedState', (Private: IPrivate) => { - const Events = Private(EventsProvider); - return class AngularPersistedState extends PersistedState { - constructor(value: any, path: any) { - super(value, path, Events); - } - }; - }); -} - function createLocalKbnUrlModule() { angular .module('app/dashboard/KbnUrl', ['app/dashboard/Private', 'ngRoute']) @@ -176,16 +148,13 @@ function createLocalKbnUrlModule() { } function createLocalConfigModule(core: AppMountContext['core']) { - angular - .module('app/dashboard/Config', ['app/dashboard/Private']) - .provider('stateManagementConfig', StateManagementConfigProvider) - .provider('config', () => { - return { - $get: () => ({ - get: core.uiSettings.get.bind(core.uiSettings), - }), - }; - }); + angular.module('app/dashboard/Config', ['app/dashboard/Private']).provider('config', () => { + return { + $get: () => ({ + get: core.uiSettings.get.bind(core.uiSettings), + }), + }; + }); } function createLocalPromiseModule() { diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/legacy_app.js b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/legacy_app.js index 2121b51ea78d85..abc0c789326f8a 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/legacy_app.js +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/legacy_app.js @@ -59,11 +59,6 @@ export function initDashboardApp(app, deps) { addHelpMenuToAppChrome(deps.chrome, deps.core.docLinks); } - app.config(stateManagementConfigProvider => { - // Dashboard state management is handled by state containers and state_sync utilities - stateManagementConfigProvider.disable(); - }); - app.factory('history', () => createHashHistory()); app.factory('kbnUrlStateStorage', history => createKbnUrlStateStorage({ @@ -141,7 +136,7 @@ export function initDashboardApp(app, deps) { }); }, resolve: { - dash: function($rootScope, $route, redirectWhenMissing, kbnUrl) { + dash: function($rootScope, $route, redirectWhenMissing, kbnUrl, history) { return ensureDefaultIndexPattern(deps.core, deps.npDataStart, $rootScope, kbnUrl).then( () => { const savedObjectsClient = deps.savedObjectsClient; @@ -160,13 +155,13 @@ export function initDashboardApp(app, deps) { dashboard.attributes.title.toLowerCase() === title.toLowerCase() ); if (matchingDashboards.length === 1) { - kbnUrl.redirect(createDashboardEditUrl(matchingDashboards[0].id)); + history.replace(createDashboardEditUrl(matchingDashboards[0].id)); } else { - kbnUrl.redirect( + history.replace( `${DashboardConstants.LANDING_PAGE_PATH}?filter="${title}"` ); + $route.reload(); } - $rootScope.$digest(); return new Promise(() => {}); }); } From 38c7d3a668feda661be15feabcb78349c5e662d6 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Fri, 31 Jan 2020 11:13:28 -0500 Subject: [PATCH 03/33] [Maps] disable style forms when they are not applied due to other style settings (#55858) * [Maps] disable style forms when they are not applied due to other style settings * disable label border color when label border is none * disable symbol style inputs when symbol size is zero Co-authored-by: Elastic Machine --- .../maps/public/layers/styles/_index.scss | 2 +- .../components/_static_dynamic_style_row.scss | 3 - .../vector/components/_style_prop_editor.scss | 3 + .../components/get_vector_style_label.js | 7 ++ .../vector_style_label_border_size_editor.js | 27 +++++- .../vector/components/style_prop_editor.js | 43 +++++++-- .../vector_style_symbolize_as_editor.js | 26 +++++- .../vector/components/vector_style_editor.js | 89 ++++++++++++++----- 8 files changed, 156 insertions(+), 44 deletions(-) delete mode 100644 x-pack/legacy/plugins/maps/public/layers/styles/vector/components/_static_dynamic_style_row.scss create mode 100644 x-pack/legacy/plugins/maps/public/layers/styles/vector/components/_style_prop_editor.scss diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/_index.scss b/x-pack/legacy/plugins/maps/public/layers/styles/_index.scss index 6d332ee878d95a..b5d9113619c769 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/_index.scss +++ b/x-pack/legacy/plugins/maps/public/layers/styles/_index.scss @@ -1,4 +1,4 @@ @import './components/color_gradient'; -@import './vector/components/static_dynamic_style_row'; +@import './vector/components/style_prop_editor'; @import './vector/components/color/color_stops'; @import './vector/components/symbol/icon_select'; diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/_static_dynamic_style_row.scss b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/_static_dynamic_style_row.scss deleted file mode 100644 index 8ec006d32a8b9b..00000000000000 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/_static_dynamic_style_row.scss +++ /dev/null @@ -1,3 +0,0 @@ -.mapStaticDynamicSylingOption__dynamicSizeHack { - width: calc(100% - #{$euiSizeXXL + $euiSizeS}); -} diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/_style_prop_editor.scss b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/_style_prop_editor.scss new file mode 100644 index 00000000000000..138605b1a7dcc3 --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/_style_prop_editor.scss @@ -0,0 +1,3 @@ +.mapStyleFormDisabledTooltip { + width: 100%; +} diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/get_vector_style_label.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/get_vector_style_label.js index c5fdda70c4eb48..35e6fa60b28e75 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/get_vector_style_label.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/get_vector_style_label.js @@ -8,6 +8,13 @@ import { i18n } from '@kbn/i18n'; import { VECTOR_STYLES } from '../vector_style_defaults'; +export function getDisabledByMessage(styleName) { + return i18n.translate('xpack.maps.styles.vector.disabledByMessage', { + defaultMessage: `Set '{styleLabel}' to enable`, + values: { styleLabel: getVectorStyleLabel(styleName) }, + }); +} + export function getVectorStyleLabel(styleName) { switch (styleName) { case VECTOR_STYLES.FILL_COLOR: diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/label/vector_style_label_border_size_editor.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/label/vector_style_label_border_size_editor.js index 7d06e8b530011b..04bb800eb1ecf9 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/label/vector_style_label_border_size_editor.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/label/vector_style_label_border_size_editor.js @@ -6,9 +6,9 @@ import React from 'react'; -import { EuiFormRow, EuiSelect } from '@elastic/eui'; +import { EuiFormRow, EuiSelect, EuiToolTip } from '@elastic/eui'; import { LABEL_BORDER_SIZES, VECTOR_STYLES } from '../../vector_style_defaults'; -import { getVectorStyleLabel } from '../get_vector_style_label'; +import { getVectorStyleLabel, getDisabledByMessage } from '../get_vector_style_label'; import { i18n } from '@kbn/i18n'; const options = [ @@ -38,7 +38,12 @@ const options = [ }, ]; -export function VectorStyleLabelBorderSizeEditor({ handlePropertyChange, styleProperty }) { +export function VectorStyleLabelBorderSizeEditor({ + disabled, + disabledBy, + handlePropertyChange, + styleProperty, +}) { function onChange(e) { const styleDescriptor = { options: { size: e.target.value }, @@ -46,12 +51,13 @@ export function VectorStyleLabelBorderSizeEditor({ handlePropertyChange, stylePr handlePropertyChange(styleProperty.getStyleName(), styleDescriptor); } - return ( + const labelBorderSizeForm = ( ); + + if (!disabled) { + return labelBorderSizeForm; + } + + return ( + + {labelBorderSizeForm} + + ); } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/style_prop_editor.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/style_prop_editor.js index e8b544d8ede161..f1180a3a564940 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/style_prop_editor.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/style_prop_editor.js @@ -5,8 +5,15 @@ */ import React, { Component, Fragment } from 'react'; -import { getVectorStyleLabel } from './get_vector_style_label'; -import { EuiFormRow, EuiSelect } from '@elastic/eui'; +import { getVectorStyleLabel, getDisabledByMessage } from './get_vector_style_label'; +import { + EuiFormRow, + EuiSelect, + EuiFlexGroup, + EuiFlexItem, + EuiFieldText, + EuiToolTip, +} from '@elastic/eui'; import { VectorStyle } from '../vector_style'; import { i18n } from '@kbn/i18n'; @@ -69,7 +76,7 @@ export class StylePropEditor extends Component { : VectorStyle.STYLE_TYPE.STATIC } onChange={this._onTypeToggle} - disabled={this.props.fields.length === 0} + disabled={this.props.disabled || this.props.fields.length === 0} aria-label={i18n.translate('xpack.maps.styles.staticDynamicSelect.ariaLabel', { defaultMessage: 'Select to style by fixed value or by data value', })} @@ -83,17 +90,35 @@ export class StylePropEditor extends Component { this._onFieldMetaOptionsChange ); + const staticDynamicSelect = this.renderStaticDynamicSelect(); + + const stylePropertyForm = this.props.disabled ? ( + + + {staticDynamicSelect} + + + + + + ) : ( + + {React.cloneElement(this.props.children, { + staticDynamicSelect, + })} + {fieldMetaOptionsPopover} + + ); + return ( - - {React.cloneElement(this.props.children, { - staticDynamicSelect: this.renderStaticDynamicSelect(), - })} - {fieldMetaOptionsPopover} - + {stylePropertyForm} ); } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/symbol/vector_style_symbolize_as_editor.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/symbol/vector_style_symbolize_as_editor.js index 9394e5c0685690..219fee311ba1b4 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/symbol/vector_style_symbolize_as_editor.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/symbol/vector_style_symbolize_as_editor.js @@ -6,11 +6,12 @@ import React from 'react'; -import { EuiFormRow, EuiButtonGroup } from '@elastic/eui'; +import { EuiFormRow, EuiButtonGroup, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { SYMBOLIZE_AS_TYPES } from '../../../../../../common/constants'; import { VECTOR_STYLES } from '../../vector_style_defaults'; +import { getDisabledByMessage } from '../get_vector_style_label'; const SYMBOLIZE_AS_OPTIONS = [ { @@ -27,7 +28,12 @@ const SYMBOLIZE_AS_OPTIONS = [ }, ]; -export function VectorStyleSymbolizeAsEditor({ styleProperty, handlePropertyChange }) { +export function VectorStyleSymbolizeAsEditor({ + disabled, + disabledBy, + styleProperty, + handlePropertyChange, +}) { const styleOptions = styleProperty.getOptions(); const selectedOption = SYMBOLIZE_AS_OPTIONS.find(({ id }) => { return id === styleOptions.value; @@ -42,7 +48,7 @@ export function VectorStyleSymbolizeAsEditor({ styleProperty, handlePropertyChan handlePropertyChange(VECTOR_STYLES.SYMBOLIZE_AS, styleDescriptor); }; - return ( + const symbolizeAsForm = ( ); + + if (!disabled) { + return symbolizeAsForm; + } + + return ( + + {symbolizeAsForm} + + ); } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js index 9299022b7895b6..441ebfb2d53bfe 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js @@ -18,6 +18,7 @@ import { OrientationEditor } from './orientation/orientation_editor'; import { getDefaultDynamicProperties, getDefaultStaticProperties, + LABEL_BORDER_SIZES, VECTOR_STYLES, } from '../vector_style_defaults'; import { DEFAULT_FILL_COLORS, DEFAULT_LINE_COLORS } from '../../color_utils'; @@ -145,9 +146,33 @@ export class VectorStyleEditor extends Component { this.props.handlePropertyChange(propertyName, styleDescriptor); }; - _renderFillColor() { + _hasBorder() { + const width = this.props.styleProperties[VECTOR_STYLES.LINE_WIDTH]; + return width.isDynamic() ? width.isComplete() : width.getOptions().size !== 0; + } + + _hasMarkerOrIcon() { + const iconSize = this.props.styleProperties[VECTOR_STYLES.ICON_SIZE]; + return !iconSize.isDynamic() && iconSize.getOptions().size > 0; + } + + _hasLabel() { + const label = this.props.styleProperties[VECTOR_STYLES.LABEL_TEXT]; + return label.isDynamic() + ? label.isComplete() + : label.getOptions().value != null && label.getOptions().value.length; + } + + _hasLabelBorder() { + const labelBorderSize = this.props.styleProperties[VECTOR_STYLES.LABEL_BORDER_SIZE]; + return labelBorderSize.getOptions().size !== LABEL_BORDER_SIZES.NONE; + } + + _renderFillColor(isPointFillColor = false) { return ( - ); - } - _renderLabelProperties() { + const hasLabel = this._hasLabel(); + const hasLabelBorder = this._hasLabelBorder(); return ( @@ -286,12 +309,15 @@ export class VectorStyleEditor extends Component { } _renderPointProperties() { + const hasMarkerOrIcon = this._hasMarkerOrIcon(); let iconOrientationEditor; let iconEditor; if (this.props.styleProperties[VECTOR_STYLES.SYMBOLIZE_AS].isSymbolizedAsIcon()) { iconOrientationEditor = ( @@ -335,18 +365,29 @@ export class VectorStyleEditor extends Component { {iconEditor} - {this._renderFillColor()} + {this._renderFillColor(true)} - {this._renderLineColor()} + {this._renderLineColor(true)} - {this._renderLineWidth()} + {this._renderLineWidth(true)} {iconOrientationEditor} - {this._renderSymbolSize()} + {this._renderLabelProperties()} From 1bb4d77479dbf5b21515378f7ab9c5d1349d4052 Mon Sep 17 00:00:00 2001 From: Kerg09 <46001886+Kerg09@users.noreply.github.com> Date: Fri, 31 Jan 2020 10:45:37 -0600 Subject: [PATCH 04/33] Adds Query.AI to known-plugins.asciidoc (#50516) Co-authored-by: Elastic Machine --- docs/plugins/known-plugins.asciidoc | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/plugins/known-plugins.asciidoc b/docs/plugins/known-plugins.asciidoc index cc27eca4c267e5..cb70a1d1c387a4 100644 --- a/docs/plugins/known-plugins.asciidoc +++ b/docs/plugins/known-plugins.asciidoc @@ -20,6 +20,7 @@ This list of plugins is not guaranteed to work on your version of Kibana. Instea * https://github.com/johtani/analyze-api-ui-plugin[Analyze UI] (johtani) - UI for elasticsearch _analyze API * https://github.com/TrumanDu/cleaner[Cleaner] (TrumanDu)- Setting index ttl. * https://github.com/bitsensor/elastalert-kibana-plugin[ElastAlert Kibana Plugin] (BitSensor) - UI to create, test and edit ElastAlert rules +* https://github.com/query-ai/queryai-kibana-plugin[AI Analyst] (Query.AI) - App providing: NLP queries, automation, ML visualizations and insights [float] === Timelion Extensions From f1068cdbff1c246c6a0154a803435a5585ef7cea Mon Sep 17 00:00:00 2001 From: Rudolf Meijering Date: Fri, 31 Jan 2020 17:49:33 +0100 Subject: [PATCH 05/33] Perform successful Elasticsearch version check before migrations (#51311) * Convert parts of Elasticsearch version check to ts * Move ES version check to NP * Improve types * Wait till for compatible ES nodes before SO migrations * Don't wait for ES compatibility if skipMigrations=true * Legacy Elasticsearch plugin integration test * Make ES compatibility check and migrations logging more visible * Test for isCompatible=false when ES version check throws * Start pollEsNodesVersion immediately * Refactor pollEsNodesVersion --- .../elasticsearch/elasticsearch_config.ts | 14 +- .../elasticsearch_service.mock.ts | 7 + .../elasticsearch_service.test.ts | 11 +- .../elasticsearch/elasticsearch_service.ts | 46 ++- src/core/server/elasticsearch/types.ts | 2 + .../version_check/ensure_es_version.test.ts | 261 ++++++++++++++++++ .../version_check/ensure_es_version.ts | 164 +++++++++++ .../es_kibana_version_compatability.test.ts} | 18 +- .../es_kibana_version_compatability.ts} | 15 +- .../integration_tests/core_services.test.ts | 8 +- .../integration_tests/legacy_service.test.ts | 2 +- .../migrations/kibana/kibana_migrator.test.ts | 6 - .../migrations/kibana/kibana_migrator.ts | 15 +- .../saved_objects_service.test.ts | 41 ++- .../saved_objects/saved_objects_service.ts | 25 +- .../core_plugins/elasticsearch/index.d.ts | 1 + .../core_plugins/elasticsearch/index.js | 14 +- .../integration_tests/elasticsearch.test.ts | 89 ++++++ .../version_health_check.js} | 24 +- .../lib/version_health_check.test.js | 71 +++++ .../server/lib/__tests__/ensure_es_version.js | 223 --------------- .../server/lib/__tests__/health_check.js | 151 ---------- .../server/lib/ensure_es_version.js | 126 --------- .../elasticsearch/server/lib/health_check.js | 75 ----- .../default_route_provider.test.ts | 2 +- .../default_route_provider_config.test.ts | 1 + .../max_payload_size.test.js | 2 +- .../on_post_auth_interceptor.test.ts | 5 +- .../on_request_interceptor.test.ts | 5 +- 29 files changed, 769 insertions(+), 655 deletions(-) create mode 100644 src/core/server/elasticsearch/version_check/ensure_es_version.test.ts create mode 100644 src/core/server/elasticsearch/version_check/ensure_es_version.ts rename src/{legacy/core_plugins/elasticsearch/server/lib/__tests__/is_es_compatible_with_kibana.js => core/server/elasticsearch/version_check/es_kibana_version_compatability.test.ts} (72%) rename src/{legacy/core_plugins/elasticsearch/server/lib/is_es_compatible_with_kibana.js => core/server/elasticsearch/version_check/es_kibana_version_compatability.ts} (76%) create mode 100644 src/legacy/core_plugins/elasticsearch/integration_tests/elasticsearch.test.ts rename src/legacy/core_plugins/elasticsearch/{server/lib/kibana_version.js => lib/version_health_check.js} (57%) create mode 100644 src/legacy/core_plugins/elasticsearch/lib/version_health_check.test.js delete mode 100644 src/legacy/core_plugins/elasticsearch/server/lib/__tests__/ensure_es_version.js delete mode 100644 src/legacy/core_plugins/elasticsearch/server/lib/__tests__/health_check.js delete mode 100644 src/legacy/core_plugins/elasticsearch/server/lib/ensure_es_version.js delete mode 100644 src/legacy/core_plugins/elasticsearch/server/lib/health_check.js diff --git a/src/core/server/elasticsearch/elasticsearch_config.ts b/src/core/server/elasticsearch/elasticsearch_config.ts index 5f06c51a53d535..50866e5550d8ec 100644 --- a/src/core/server/elasticsearch/elasticsearch_config.ts +++ b/src/core/server/elasticsearch/elasticsearch_config.ts @@ -103,7 +103,19 @@ const configSchema = schema.object({ ), apiVersion: schema.string({ defaultValue: DEFAULT_API_VERSION }), healthCheck: schema.object({ delay: schema.duration({ defaultValue: 2500 }) }), - ignoreVersionMismatch: schema.boolean({ defaultValue: false }), + ignoreVersionMismatch: schema.conditional( + schema.contextRef('dev'), + false, + schema.boolean({ + validate: rawValue => { + if (rawValue === true) { + return '"ignoreVersionMismatch" can only be set to true in development mode'; + } + }, + defaultValue: false, + }), + schema.boolean({ defaultValue: false }) + ), }); const deprecations: ConfigDeprecationProvider = () => [ diff --git a/src/core/server/elasticsearch/elasticsearch_service.mock.ts b/src/core/server/elasticsearch/elasticsearch_service.mock.ts index a4e51ca55b3e71..b8ad3754965449 100644 --- a/src/core/server/elasticsearch/elasticsearch_service.mock.ts +++ b/src/core/server/elasticsearch/elasticsearch_service.mock.ts @@ -23,6 +23,7 @@ import { IScopedClusterClient } from './scoped_cluster_client'; import { ElasticsearchConfig } from './elasticsearch_config'; import { ElasticsearchService } from './elasticsearch_service'; import { InternalElasticsearchServiceSetup, ElasticsearchServiceSetup } from './types'; +import { NodesVersionCompatibility } from './version_check/ensure_es_version'; const createScopedClusterClientMock = (): jest.Mocked => ({ callAsInternalUser: jest.fn(), @@ -71,6 +72,12 @@ type MockedInternalElasticSearchServiceSetup = jest.Mocked< const createInternalSetupContractMock = () => { const setupContract: MockedInternalElasticSearchServiceSetup = { ...createSetupContractMock(), + esNodesCompatibility$: new BehaviorSubject({ + isCompatible: true, + incompatibleNodes: [], + warningNodes: [], + kibanaVersion: '8.0.0', + }), legacy: { config$: new BehaviorSubject({} as ElasticsearchConfig), }, diff --git a/src/core/server/elasticsearch/elasticsearch_service.test.ts b/src/core/server/elasticsearch/elasticsearch_service.test.ts index 5a7d223fec7ad9..022a03e01d37df 100644 --- a/src/core/server/elasticsearch/elasticsearch_service.test.ts +++ b/src/core/server/elasticsearch/elasticsearch_service.test.ts @@ -31,6 +31,7 @@ import { httpServiceMock } from '../http/http_service.mock'; import { ElasticsearchConfig } from './elasticsearch_config'; import { ElasticsearchService } from './elasticsearch_service'; import { elasticsearchServiceMock } from './elasticsearch_service.mock'; +import { duration } from 'moment'; let elasticsearchService: ElasticsearchService; const configService = configServiceMock.create(); @@ -41,7 +42,7 @@ configService.atPath.mockReturnValue( new BehaviorSubject({ hosts: ['http://1.2.3.4'], healthCheck: { - delay: 2000, + delay: duration(2000), }, ssl: { verificationMode: 'none', @@ -125,7 +126,7 @@ describe('#setup', () => { const config = MockClusterClient.mock.calls[0][0]; expect(config).toMatchInlineSnapshot(` Object { - "healthCheckDelay": 2000, + "healthCheckDelay": "PT2S", "hosts": Array [ "http://8.8.8.8", ], @@ -150,7 +151,7 @@ Object { const config = MockClusterClient.mock.calls[0][0]; expect(config).toMatchInlineSnapshot(` Object { - "healthCheckDelay": 2000, + "healthCheckDelay": "PT2S", "hosts": Array [ "http://1.2.3.4", ], @@ -174,7 +175,7 @@ Object { new BehaviorSubject({ hosts: ['http://1.2.3.4', 'http://9.8.7.6'], healthCheck: { - delay: 2000, + delay: duration(2000), }, ssl: { verificationMode: 'none', @@ -196,7 +197,7 @@ Object { const config = MockClusterClient.mock.calls[0][0]; expect(config).toMatchInlineSnapshot(` Object { - "healthCheckDelay": 2000, + "healthCheckDelay": "PT2S", "hosts": Array [ "http://8.8.8.8", ], diff --git a/src/core/server/elasticsearch/elasticsearch_service.ts b/src/core/server/elasticsearch/elasticsearch_service.ts index de111e1cb8b9b9..9eaf125cc006fc 100644 --- a/src/core/server/elasticsearch/elasticsearch_service.ts +++ b/src/core/server/elasticsearch/elasticsearch_service.ts @@ -30,6 +30,7 @@ import { ElasticsearchConfig, ElasticsearchConfigType } from './elasticsearch_co import { InternalHttpServiceSetup, GetAuthHeaders } from '../http/'; import { InternalElasticsearchServiceSetup } from './types'; import { CallAPIOptions } from './api_types'; +import { pollEsNodesVersion } from './version_check/ensure_es_version'; /** @internal */ interface CoreClusterClients { @@ -46,9 +47,17 @@ interface SetupDeps { export class ElasticsearchService implements CoreService { private readonly log: Logger; private readonly config$: Observable; - private subscription?: Subscription; + private subscriptions: { + client?: Subscription; + esNodesCompatibility?: Subscription; + } = { + client: undefined, + esNodesCompatibility: undefined, + }; + private kibanaVersion: string; constructor(private readonly coreContext: CoreContext) { + this.kibanaVersion = coreContext.env.packageInfo.version; this.log = coreContext.logger.get('elasticsearch-service'); this.config$ = coreContext.configService .atPath('elasticsearch') @@ -60,7 +69,7 @@ export class ElasticsearchService implements CoreService { - if (this.subscription !== undefined) { + if (this.subscriptions.client !== undefined) { this.log.error('Clients cannot be changed after they are created'); return false; } @@ -91,7 +100,7 @@ export class ElasticsearchService implements CoreService; - this.subscription = clients$.connect(); + this.subscriptions.client = clients$.connect(); const config = await this.config$.pipe(first()).toPromise(); @@ -149,11 +158,31 @@ export class ElasticsearchService implements CoreService).connect(); + + // TODO: Move to Status Service https://github.com/elastic/kibana/issues/41983 + esNodesCompatibility$.subscribe(({ isCompatible, message }) => { + if (!isCompatible && message) { + this.log.error(message); + } + }); + return { legacy: { config$: clients$.pipe(map(clients => clients.config)) }, adminClient, dataClient, + esNodesCompatibility$, createClient: (type: string, clientConfig: Partial = {}) => { const finalConfig = merge({}, config, clientConfig); @@ -166,11 +195,12 @@ export class ElasticsearchService implements CoreService; }; + esNodesCompatibility$: Observable; } diff --git a/src/core/server/elasticsearch/version_check/ensure_es_version.test.ts b/src/core/server/elasticsearch/version_check/ensure_es_version.test.ts new file mode 100644 index 00000000000000..4989c4a31295cb --- /dev/null +++ b/src/core/server/elasticsearch/version_check/ensure_es_version.test.ts @@ -0,0 +1,261 @@ +/* + * 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 { mapNodesVersionCompatibility, pollEsNodesVersion, NodesInfo } from './ensure_es_version'; +import { loggingServiceMock } from '../../logging/logging_service.mock'; +import { take, delay } from 'rxjs/operators'; +import { TestScheduler } from 'rxjs/testing'; +import { of } from 'rxjs'; + +const mockLoggerFactory = loggingServiceMock.create(); +const mockLogger = mockLoggerFactory.get('mock logger'); + +const KIBANA_VERSION = '5.1.0'; + +function createNodes(...versions: string[]): NodesInfo { + const nodes = {} as any; + versions + .map(version => { + return { + version, + http: { + publish_address: 'http_address', + }, + ip: 'ip', + }; + }) + .forEach((node, i) => { + nodes[`node-${i}`] = node; + }); + + return { nodes }; +} + +describe('mapNodesVersionCompatibility', () => { + function createNodesInfoWithoutHTTP(version: string): NodesInfo { + return { nodes: { 'node-without-http': { version, ip: 'ip' } } } as any; + } + + it('returns isCompatible=true with a single node that matches', async () => { + const nodesInfo = createNodes('5.1.0'); + const result = await mapNodesVersionCompatibility(nodesInfo, KIBANA_VERSION, false); + expect(result.isCompatible).toBe(true); + }); + + it('returns isCompatible=true with multiple nodes that satisfy', async () => { + const nodesInfo = createNodes('5.1.0', '5.2.0', '5.1.1-Beta1'); + const result = await mapNodesVersionCompatibility(nodesInfo, KIBANA_VERSION, false); + expect(result.isCompatible).toBe(true); + }); + + it('returns isCompatible=false for a single node that is out of date', () => { + // 5.0.0 ES is too old to work with a 5.1.0 version of Kibana. + const nodesInfo = createNodes('5.1.0', '5.2.0', '5.0.0'); + const result = mapNodesVersionCompatibility(nodesInfo, KIBANA_VERSION, false); + expect(result.isCompatible).toBe(false); + expect(result.message).toMatchInlineSnapshot( + `"This version of Kibana (v5.1.0) is incompatible with the following Elasticsearch nodes in your cluster: v5.0.0 @ http_address (ip)"` + ); + }); + + it('returns isCompatible=false for an incompatible node without http publish address', async () => { + const nodesInfo = createNodesInfoWithoutHTTP('6.1.1'); + const result = mapNodesVersionCompatibility(nodesInfo, KIBANA_VERSION, false); + expect(result.isCompatible).toBe(false); + expect(result.message).toMatchInlineSnapshot( + `"This version of Kibana (v5.1.0) is incompatible with the following Elasticsearch nodes in your cluster: v6.1.1 @ undefined (ip)"` + ); + }); + + it('returns isCompatible=true for outdated nodes when ignoreVersionMismatch=true', async () => { + // 5.0.0 ES is too old to work with a 5.1.0 version of Kibana. + const nodesInfo = createNodes('5.1.0', '5.2.0', '5.0.0'); + const ignoreVersionMismatch = true; + const result = mapNodesVersionCompatibility(nodesInfo, KIBANA_VERSION, ignoreVersionMismatch); + expect(result.isCompatible).toBe(true); + expect(result.message).toMatchInlineSnapshot( + `"Ignoring version incompatibility between Kibana v5.1.0 and the following Elasticsearch nodes: v5.0.0 @ http_address (ip)"` + ); + }); + + it('returns isCompatible=true with a message if a node is only off by a patch version', () => { + const result = mapNodesVersionCompatibility(createNodes('5.1.1'), KIBANA_VERSION, false); + expect(result.isCompatible).toBe(true); + expect(result.message).toMatchInlineSnapshot( + `"You're running Kibana 5.1.0 with some different versions of Elasticsearch. Update Kibana or Elasticsearch to the same version to prevent compatibility issues: v5.1.1 @ http_address (ip)"` + ); + }); + + it('returns isCompatible=true with a message if a node is only off by a patch version and without http publish address', async () => { + const result = mapNodesVersionCompatibility(createNodes('5.1.1'), KIBANA_VERSION, false); + expect(result.isCompatible).toBe(true); + expect(result.message).toMatchInlineSnapshot( + `"You're running Kibana 5.1.0 with some different versions of Elasticsearch. Update Kibana or Elasticsearch to the same version to prevent compatibility issues: v5.1.1 @ http_address (ip)"` + ); + }); +}); + +describe('pollEsNodesVersion', () => { + const callWithInternalUser = jest.fn(); + const getTestScheduler = () => + new TestScheduler((actual, expected) => { + expect(actual).toEqual(expected); + }); + + beforeEach(() => { + callWithInternalUser.mockClear(); + }); + + it('returns iscCompatible=false and keeps polling when a poll request throws', done => { + expect.assertions(3); + const expectedCompatibilityResults = [false, false, true]; + jest.clearAllMocks(); + callWithInternalUser.mockResolvedValueOnce(createNodes('5.1.0', '5.2.0', '5.0.0')); + callWithInternalUser.mockRejectedValueOnce(new Error('mock request error')); + callWithInternalUser.mockResolvedValueOnce(createNodes('5.1.0', '5.2.0', '5.1.1-Beta1')); + pollEsNodesVersion({ + callWithInternalUser, + esVersionCheckInterval: 1, + ignoreVersionMismatch: false, + kibanaVersion: KIBANA_VERSION, + log: mockLogger, + }) + .pipe(take(3)) + .subscribe({ + next: result => { + expect(result.isCompatible).toBe(expectedCompatibilityResults.shift()); + }, + complete: done, + error: done, + }); + }); + + it('returns compatibility results', done => { + expect.assertions(1); + const nodes = createNodes('5.1.0', '5.2.0', '5.0.0'); + callWithInternalUser.mockResolvedValueOnce(nodes); + pollEsNodesVersion({ + callWithInternalUser, + esVersionCheckInterval: 1, + ignoreVersionMismatch: false, + kibanaVersion: KIBANA_VERSION, + log: mockLogger, + }) + .pipe(take(1)) + .subscribe({ + next: result => { + expect(result).toEqual(mapNodesVersionCompatibility(nodes, KIBANA_VERSION, false)); + }, + complete: done, + error: done, + }); + }); + + it('only emits if the node versions changed since the previous poll', done => { + expect.assertions(4); + callWithInternalUser.mockResolvedValueOnce(createNodes('5.1.0', '5.2.0', '5.0.0')); // emit + callWithInternalUser.mockResolvedValueOnce(createNodes('5.0.0', '5.1.0', '5.2.0')); // ignore, same versions, different ordering + callWithInternalUser.mockResolvedValueOnce(createNodes('5.1.1', '5.2.0', '5.0.0')); // emit + callWithInternalUser.mockResolvedValueOnce(createNodes('5.1.1', '5.1.2', '5.1.3')); // emit + callWithInternalUser.mockResolvedValueOnce(createNodes('5.1.1', '5.1.2', '5.1.3')); // ignore + callWithInternalUser.mockResolvedValueOnce(createNodes('5.0.0', '5.1.0', '5.2.0')); // emit, different from previous version + + pollEsNodesVersion({ + callWithInternalUser, + esVersionCheckInterval: 1, + ignoreVersionMismatch: false, + kibanaVersion: KIBANA_VERSION, + log: mockLogger, + }) + .pipe(take(4)) + .subscribe({ + next: result => expect(result).toBeDefined(), + complete: done, + error: done, + }); + }); + + it('starts polling immediately and then every esVersionCheckInterval', () => { + expect.assertions(1); + callWithInternalUser.mockReturnValueOnce([createNodes('5.1.0', '5.2.0', '5.0.0')]); + callWithInternalUser.mockReturnValueOnce([createNodes('5.1.1', '5.2.0', '5.0.0')]); + + getTestScheduler().run(({ expectObservable }) => { + const expected = 'a 99ms (b|)'; + + const esNodesCompatibility$ = pollEsNodesVersion({ + callWithInternalUser, + esVersionCheckInterval: 100, + ignoreVersionMismatch: false, + kibanaVersion: KIBANA_VERSION, + log: mockLogger, + }).pipe(take(2)); + + expectObservable(esNodesCompatibility$).toBe(expected, { + a: mapNodesVersionCompatibility( + createNodes('5.1.0', '5.2.0', '5.0.0'), + KIBANA_VERSION, + false + ), + b: mapNodesVersionCompatibility( + createNodes('5.1.1', '5.2.0', '5.0.0'), + KIBANA_VERSION, + false + ), + }); + }); + }); + + it('waits for es version check requests to complete before scheduling the next one', () => { + expect.assertions(2); + + getTestScheduler().run(({ expectObservable }) => { + const expected = '100ms a 99ms (b|)'; + + callWithInternalUser.mockReturnValueOnce( + of(createNodes('5.1.0', '5.2.0', '5.0.0')).pipe(delay(100)) + ); + callWithInternalUser.mockReturnValueOnce( + of(createNodes('5.1.1', '5.2.0', '5.0.0')).pipe(delay(100)) + ); + + const esNodesCompatibility$ = pollEsNodesVersion({ + callWithInternalUser, + esVersionCheckInterval: 10, + ignoreVersionMismatch: false, + kibanaVersion: KIBANA_VERSION, + log: mockLogger, + }).pipe(take(2)); + + expectObservable(esNodesCompatibility$).toBe(expected, { + a: mapNodesVersionCompatibility( + createNodes('5.1.0', '5.2.0', '5.0.0'), + KIBANA_VERSION, + false + ), + b: mapNodesVersionCompatibility( + createNodes('5.1.1', '5.2.0', '5.0.0'), + KIBANA_VERSION, + false + ), + }); + }); + + expect(callWithInternalUser).toHaveBeenCalledTimes(2); + }); +}); diff --git a/src/core/server/elasticsearch/version_check/ensure_es_version.ts b/src/core/server/elasticsearch/version_check/ensure_es_version.ts new file mode 100644 index 00000000000000..3e760ec0efabd6 --- /dev/null +++ b/src/core/server/elasticsearch/version_check/ensure_es_version.ts @@ -0,0 +1,164 @@ +/* + * 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. + */ + +/** + * ES and Kibana versions are locked, so Kibana should require that ES has the same version as + * that defined in Kibana's package.json. + */ + +import { timer, of, from, Observable } from 'rxjs'; +import { map, distinctUntilChanged, catchError, exhaustMap } from 'rxjs/operators'; +import { + esVersionCompatibleWithKibana, + esVersionEqualsKibana, +} from './es_kibana_version_compatability'; +import { Logger } from '../../logging'; +import { APICaller } from '..'; + +export interface PollEsNodesVersionOptions { + callWithInternalUser: APICaller; + log: Logger; + kibanaVersion: string; + ignoreVersionMismatch: boolean; + esVersionCheckInterval: number; +} + +interface NodeInfo { + version: string; + ip: string; + http: { + publish_address: string; + }; + name: string; +} + +export interface NodesInfo { + nodes: { + [key: string]: NodeInfo; + }; +} + +export interface NodesVersionCompatibility { + isCompatible: boolean; + message?: string; + incompatibleNodes: NodeInfo[]; + warningNodes: NodeInfo[]; + kibanaVersion: string; +} + +function getHumanizedNodeName(node: NodeInfo) { + const publishAddress = node?.http?.publish_address + ' ' || ''; + return 'v' + node.version + ' @ ' + publishAddress + '(' + node.ip + ')'; +} + +export function mapNodesVersionCompatibility( + nodesInfo: NodesInfo, + kibanaVersion: string, + ignoreVersionMismatch: boolean +): NodesVersionCompatibility { + if (Object.keys(nodesInfo.nodes).length === 0) { + return { + isCompatible: false, + message: 'Unable to retrieve version information from Elasticsearch nodes.', + incompatibleNodes: [], + warningNodes: [], + kibanaVersion, + }; + } + const nodes = Object.keys(nodesInfo.nodes) + .sort() // Sorting ensures a stable node ordering for comparison + .map(key => nodesInfo.nodes[key]) + .map(node => Object.assign({}, node, { name: getHumanizedNodeName(node) })); + + // Aggregate incompatible ES nodes. + const incompatibleNodes = nodes.filter( + node => !esVersionCompatibleWithKibana(node.version, kibanaVersion) + ); + + // Aggregate ES nodes which should prompt a Kibana upgrade. It's acceptable + // if ES and Kibana versions are not the same as long as they are not + // incompatible, but we should warn about it. + // Ignore version qualifiers https://github.com/elastic/elasticsearch/issues/36859 + const warningNodes = nodes.filter(node => !esVersionEqualsKibana(node.version, kibanaVersion)); + + // Note: If incompatible and warning nodes are present `message` only contains + // an incompatibility notice. + let message; + if (incompatibleNodes.length > 0) { + const incompatibleNodeNames = incompatibleNodes.map(node => node.name).join(', '); + if (ignoreVersionMismatch) { + message = `Ignoring version incompatibility between Kibana v${kibanaVersion} and the following Elasticsearch nodes: ${incompatibleNodeNames}`; + } else { + message = `This version of Kibana (v${kibanaVersion}) is incompatible with the following Elasticsearch nodes in your cluster: ${incompatibleNodeNames}`; + } + } else if (warningNodes.length > 0) { + const warningNodeNames = warningNodes.map(node => node.name).join(', '); + message = + `You're running Kibana ${kibanaVersion} with some different versions of ` + + 'Elasticsearch. Update Kibana or Elasticsearch to the same ' + + `version to prevent compatibility issues: ${warningNodeNames}`; + } + + return { + isCompatible: ignoreVersionMismatch || incompatibleNodes.length === 0, + message, + incompatibleNodes, + warningNodes, + kibanaVersion, + }; +} + +// Returns true if two NodesVersionCompatibility entries match +function compareNodes(prev: NodesVersionCompatibility, curr: NodesVersionCompatibility) { + const nodesEqual = (n: NodeInfo, m: NodeInfo) => n.ip === m.ip && n.version === m.version; + return ( + curr.isCompatible === prev.isCompatible && + curr.incompatibleNodes.length === prev.incompatibleNodes.length && + curr.warningNodes.length === prev.warningNodes.length && + curr.incompatibleNodes.every((node, i) => nodesEqual(node, prev.incompatibleNodes[i])) && + curr.warningNodes.every((node, i) => nodesEqual(node, prev.warningNodes[i])) + ); +} + +export const pollEsNodesVersion = ({ + callWithInternalUser, + log, + kibanaVersion, + ignoreVersionMismatch, + esVersionCheckInterval: healthCheckInterval, +}: PollEsNodesVersionOptions): Observable => { + log.debug('Checking Elasticsearch version'); + return timer(0, healthCheckInterval).pipe( + exhaustMap(() => { + return from( + callWithInternalUser('nodes.info', { + filterPath: ['nodes.*.version', 'nodes.*.http.publish_address', 'nodes.*.ip'], + }) + ).pipe( + catchError(_err => { + return of({ nodes: {} }); + }) + ); + }), + map((nodesInfo: NodesInfo) => + mapNodesVersionCompatibility(nodesInfo, kibanaVersion, ignoreVersionMismatch) + ), + distinctUntilChanged(compareNodes) // Only emit if there are new nodes or versions + ); +}; diff --git a/src/legacy/core_plugins/elasticsearch/server/lib/__tests__/is_es_compatible_with_kibana.js b/src/core/server/elasticsearch/version_check/es_kibana_version_compatability.test.ts similarity index 72% rename from src/legacy/core_plugins/elasticsearch/server/lib/__tests__/is_es_compatible_with_kibana.js rename to src/core/server/elasticsearch/version_check/es_kibana_version_compatability.test.ts index 092c0ecf1071ca..152f25c8138819 100644 --- a/src/legacy/core_plugins/elasticsearch/server/lib/__tests__/is_es_compatible_with_kibana.js +++ b/src/core/server/elasticsearch/version_check/es_kibana_version_compatability.test.ts @@ -17,41 +17,39 @@ * under the License. */ -import expect from '@kbn/expect'; - -import isEsCompatibleWithKibana from '../is_es_compatible_with_kibana'; +import { esVersionCompatibleWithKibana } from './es_kibana_version_compatability'; describe('plugins/elasticsearch', () => { describe('lib/is_es_compatible_with_kibana', () => { describe('returns false', () => { it('when ES major is greater than Kibana major', () => { - expect(isEsCompatibleWithKibana('1.0.0', '0.0.0')).to.be(false); + expect(esVersionCompatibleWithKibana('1.0.0', '0.0.0')).toBe(false); }); it('when ES major is less than Kibana major', () => { - expect(isEsCompatibleWithKibana('0.0.0', '1.0.0')).to.be(false); + expect(esVersionCompatibleWithKibana('0.0.0', '1.0.0')).toBe(false); }); it('when majors are equal, but ES minor is less than Kibana minor', () => { - expect(isEsCompatibleWithKibana('1.0.0', '1.1.0')).to.be(false); + expect(esVersionCompatibleWithKibana('1.0.0', '1.1.0')).toBe(false); }); }); describe('returns true', () => { it('when version numbers are the same', () => { - expect(isEsCompatibleWithKibana('1.1.1', '1.1.1')).to.be(true); + expect(esVersionCompatibleWithKibana('1.1.1', '1.1.1')).toBe(true); }); it('when majors are equal, and ES minor is greater than Kibana minor', () => { - expect(isEsCompatibleWithKibana('1.1.0', '1.0.0')).to.be(true); + expect(esVersionCompatibleWithKibana('1.1.0', '1.0.0')).toBe(true); }); it('when majors and minors are equal, and ES patch is greater than Kibana patch', () => { - expect(isEsCompatibleWithKibana('1.1.1', '1.1.0')).to.be(true); + expect(esVersionCompatibleWithKibana('1.1.1', '1.1.0')).toBe(true); }); it('when majors and minors are equal, but ES patch is less than Kibana patch', () => { - expect(isEsCompatibleWithKibana('1.1.0', '1.1.1')).to.be(true); + expect(esVersionCompatibleWithKibana('1.1.0', '1.1.1')).toBe(true); }); }); }); diff --git a/src/legacy/core_plugins/elasticsearch/server/lib/is_es_compatible_with_kibana.js b/src/core/server/elasticsearch/version_check/es_kibana_version_compatability.ts similarity index 76% rename from src/legacy/core_plugins/elasticsearch/server/lib/is_es_compatible_with_kibana.js rename to src/core/server/elasticsearch/version_check/es_kibana_version_compatability.ts index 4afbd488d2946f..28b9c0a23e672d 100644 --- a/src/legacy/core_plugins/elasticsearch/server/lib/is_es_compatible_with_kibana.js +++ b/src/core/server/elasticsearch/version_check/es_kibana_version_compatability.ts @@ -17,15 +17,14 @@ * under the License. */ +import semver, { coerce } from 'semver'; + /** - * Let's weed out the ES versions that won't work with a given Kibana version. + * Checks for the compatibilitiy between Elasticsearch and Kibana versions * 1. Major version differences will never work together. * 2. Older versions of ES won't work with newer versions of Kibana. */ - -import semver from 'semver'; - -export default function isEsCompatibleWithKibana(esVersion, kibanaVersion) { +export function esVersionCompatibleWithKibana(esVersion: string, kibanaVersion: string) { const esVersionNumbers = { major: semver.major(esVersion), minor: semver.minor(esVersion), @@ -50,3 +49,9 @@ export default function isEsCompatibleWithKibana(esVersion, kibanaVersion) { return true; } + +export function esVersionEqualsKibana(nodeVersion: string, kibanaVersion: string) { + const nodeSemVer = coerce(nodeVersion); + const kibanaSemver = coerce(kibanaVersion); + return nodeSemVer && kibanaSemver && nodeSemVer.version === kibanaSemver.version; +} diff --git a/src/core/server/http/integration_tests/core_services.test.ts b/src/core/server/http/integration_tests/core_services.test.ts index 65b8ba551cf916..425d8cac1893ea 100644 --- a/src/core/server/http/integration_tests/core_services.test.ts +++ b/src/core/server/http/integration_tests/core_services.test.ts @@ -43,7 +43,7 @@ describe('http service', () => { describe('auth', () => { let root: ReturnType; beforeEach(async () => { - root = kbnTestServer.createRoot(); + root = kbnTestServer.createRoot({ migrations: { skip: true } }); }, 30000); afterEach(async () => { @@ -161,7 +161,7 @@ describe('http service', () => { let root: ReturnType; beforeEach(async () => { - root = kbnTestServer.createRoot(); + root = kbnTestServer.createRoot({ migrations: { skip: true } }); }, 30000); afterEach(async () => { @@ -295,7 +295,7 @@ describe('http service', () => { describe('#basePath()', () => { let root: ReturnType; beforeEach(async () => { - root = kbnTestServer.createRoot(); + root = kbnTestServer.createRoot({ migrations: { skip: true } }); }, 30000); afterEach(async () => await root.shutdown()); @@ -324,7 +324,7 @@ describe('http service', () => { describe('elasticsearch', () => { let root: ReturnType; beforeEach(async () => { - root = kbnTestServer.createRoot(); + root = kbnTestServer.createRoot({ migrations: { skip: true } }); }, 30000); afterEach(async () => { diff --git a/src/core/server/legacy/integration_tests/legacy_service.test.ts b/src/core/server/legacy/integration_tests/legacy_service.test.ts index da2550f2ae799a..e8bcf7a42d192f 100644 --- a/src/core/server/legacy/integration_tests/legacy_service.test.ts +++ b/src/core/server/legacy/integration_tests/legacy_service.test.ts @@ -22,7 +22,7 @@ describe('legacy service', () => { describe('http server', () => { let root: ReturnType; beforeEach(() => { - root = kbnTestServer.createRoot(); + root = kbnTestServer.createRoot({ migrations: { skip: true } }); }, 30000); afterEach(async () => await root.shutdown()); diff --git a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts index b89abc596ad18f..c6a72eb53d6c4f 100644 --- a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts +++ b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts @@ -59,12 +59,6 @@ describe('KibanaMigrator', () => { }); describe('runMigrations', () => { - it('resolves isMigrated if migrations were skipped', async () => { - const skipMigrations = true; - const result = await new KibanaMigrator(mockOptions()).runMigrations(skipMigrations); - expect(result).toEqual([{ status: 'skipped' }, { status: 'skipped' }]); - }); - it('only runs migrations once if called multiple times', async () => { const options = mockOptions(); const clusterStub = jest.fn(() => ({ status: 404 })); diff --git a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts index c35e8dd90b5b14..747b48a540109e 100644 --- a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts +++ b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts @@ -107,24 +107,15 @@ export class KibanaMigrator { * The promise resolves with an array of migration statuses, one for each * elasticsearch index which was migrated. */ - public runMigrations(skipMigrations: boolean = false): Promise> { + public runMigrations(): Promise> { if (this.migrationResult === undefined) { - this.migrationResult = this.runMigrationsInternal(skipMigrations); + this.migrationResult = this.runMigrationsInternal(); } return this.migrationResult; } - private runMigrationsInternal(skipMigrations: boolean) { - if (skipMigrations) { - this.log.warn( - 'Skipping Saved Object migrations on startup. Note: Individual documents will still be migrated when read or written.' - ); - return Promise.resolve( - Object.keys(this.mappingProperties).map(() => ({ status: 'skipped' })) - ); - } - + private runMigrationsInternal() { const kibanaIndexName = this.kibanaConfig.index; const indexMap = createIndexMap({ config: this.config, diff --git a/src/core/server/saved_objects/saved_objects_service.test.ts b/src/core/server/saved_objects/saved_objects_service.test.ts index 6668d57045a957..19798aa68928db 100644 --- a/src/core/server/saved_objects/saved_objects_service.test.ts +++ b/src/core/server/saved_objects/saved_objects_service.test.ts @@ -31,11 +31,14 @@ import { configServiceMock } from '../mocks'; import { elasticsearchServiceMock } from '../elasticsearch/elasticsearch_service.mock'; import { legacyServiceMock } from '../legacy/legacy_service.mock'; import { SavedObjectsClientFactoryProvider } from './service/lib'; +import { BehaviorSubject } from 'rxjs'; +import { NodesVersionCompatibility } from '../elasticsearch/version_check/ensure_es_version'; describe('SavedObjectsService', () => { const createSetupDeps = () => { + const elasticsearchMock = elasticsearchServiceMock.createInternalSetup(); return { - elasticsearch: elasticsearchServiceMock.createInternalSetup(), + elasticsearch: elasticsearchMock, legacyPlugins: legacyServiceMock.createDiscoverPlugins(), }; }; @@ -137,7 +140,7 @@ describe('SavedObjectsService', () => { await soService.setup(createSetupDeps()); await soService.start({}); - expect(migratorInstanceMock.runMigrations).toHaveBeenCalledWith(true); + expect(migratorInstanceMock.runMigrations).not.toHaveBeenCalled(); }); it('skips KibanaMigrator migrations when migrations.skip=true', async () => { @@ -146,7 +149,38 @@ describe('SavedObjectsService', () => { const soService = new SavedObjectsService(coreContext); await soService.setup(createSetupDeps()); await soService.start({}); - expect(migratorInstanceMock.runMigrations).toHaveBeenCalledWith(true); + expect(migratorInstanceMock.runMigrations).not.toHaveBeenCalled(); + }); + + it('waits for all es nodes to be compatible before running migrations', async done => { + expect.assertions(2); + const configService = configServiceMock.create({ atPath: { skip: false } }); + const coreContext = mockCoreContext.create({ configService }); + const soService = new SavedObjectsService(coreContext); + const setupDeps = createSetupDeps(); + // Create an new subject so that we can control when isCompatible=true + // is emitted. + setupDeps.elasticsearch.esNodesCompatibility$ = new BehaviorSubject({ + isCompatible: false, + incompatibleNodes: [], + warningNodes: [], + kibanaVersion: '8.0.0', + }); + await soService.setup(setupDeps); + soService.start({}); + expect(migratorInstanceMock.runMigrations).toHaveBeenCalledTimes(0); + ((setupDeps.elasticsearch.esNodesCompatibility$ as any) as BehaviorSubject< + NodesVersionCompatibility + >).next({ + isCompatible: true, + incompatibleNodes: [], + warningNodes: [], + kibanaVersion: '8.0.0', + }); + setImmediate(() => { + expect(migratorInstanceMock.runMigrations).toHaveBeenCalledTimes(1); + done(); + }); }); it('resolves with KibanaMigrator after waiting for migrations to complete', async () => { @@ -158,7 +192,6 @@ describe('SavedObjectsService', () => { const startContract = await soService.start({}); expect(startContract.migrator).toBe(migratorInstanceMock); - expect(migratorInstanceMock.runMigrations).toHaveBeenCalledWith(false); expect(migratorInstanceMock.runMigrations).toHaveBeenCalledTimes(1); }); }); diff --git a/src/core/server/saved_objects/saved_objects_service.ts b/src/core/server/saved_objects/saved_objects_service.ts index b08033a19242b0..0c985c71c7e2f3 100644 --- a/src/core/server/saved_objects/saved_objects_service.ts +++ b/src/core/server/saved_objects/saved_objects_service.ts @@ -18,7 +18,7 @@ */ import { CoreService } from 'src/core/types'; -import { first } from 'rxjs/operators'; +import { first, filter, take } from 'rxjs/operators'; import { SavedObjectsClient, SavedObjectsSchema, @@ -283,9 +283,22 @@ export class SavedObjectsService const cliArgs = this.coreContext.env.cliArgs; const skipMigrations = cliArgs.optimize || savedObjectsConfig.skip; - this.logger.debug('Starting saved objects migration'); - await migrator.runMigrations(skipMigrations); - this.logger.debug('Saved objects migration completed'); + if (skipMigrations) { + this.logger.warn( + 'Skipping Saved Object migrations on startup. Note: Individual documents will still be migrated when read or written.' + ); + } else { + this.logger.info( + 'Waiting until all Elasticsearch nodes are compatible with Kibana before starting saved objects migrations...' + ); + await this.setupDeps!.elasticsearch.esNodesCompatibility$.pipe( + filter(nodes => nodes.isCompatible), + take(1) + ).toPromise(); + + this.logger.info('Starting saved objects migrations'); + await migrator.runMigrations(); + } const createRepository = (callCluster: APICaller, extraTypes: string[] = []) => { return SavedObjectsRepository.createRepository( @@ -343,14 +356,14 @@ export class SavedObjectsService savedObjectMappings: this.mappings, savedObjectMigrations: this.migrations, savedObjectValidations: this.validations, - logger: this.coreContext.logger.get('migrations'), + logger: this.logger, kibanaVersion: this.coreContext.env.packageInfo.version, config: this.setupDeps!.legacyPlugins.pluginExtendedConfig, savedObjectsConfig, kibanaConfig, callCluster: migrationsRetryCallCluster( adminClient.callAsInternalUser, - this.coreContext.logger.get('migrations'), + this.logger, migrationsRetryDelay ), }); diff --git a/src/legacy/core_plugins/elasticsearch/index.d.ts b/src/legacy/core_plugins/elasticsearch/index.d.ts index 4cbb1c82cc1e40..df713160137a6a 100644 --- a/src/legacy/core_plugins/elasticsearch/index.d.ts +++ b/src/legacy/core_plugins/elasticsearch/index.d.ts @@ -523,6 +523,7 @@ export interface CallCluster { } export interface ElasticsearchPlugin { + status: { on: (status: string, cb: () => void) => void }; getCluster(name: string): Cluster; createCluster(name: string, config: ClusterConfig): Cluster; waitUntilReady(): Promise; diff --git a/src/legacy/core_plugins/elasticsearch/index.js b/src/legacy/core_plugins/elasticsearch/index.js index 5872a33d8aa082..35dd6562aed984 100644 --- a/src/legacy/core_plugins/elasticsearch/index.js +++ b/src/legacy/core_plugins/elasticsearch/index.js @@ -17,10 +17,10 @@ * under the License. */ import { first } from 'rxjs/operators'; -import healthCheck from './server/lib/health_check'; import { Cluster } from './server/lib/cluster'; import { createProxy } from './server/lib/create_proxy'; import { handleESError } from './server/lib/handle_es_error'; +import { versionHealthCheck } from './lib/version_health_check'; export default function(kibana) { let defaultVars; @@ -92,15 +92,13 @@ export default function(kibana) { createProxy(server); - // Set up the health check service and start it. - const { start, waitUntilReady } = healthCheck( + const waitUntilHealthy = versionHealthCheck( this, - server, - esConfig.healthCheckDelay.asMilliseconds(), - esConfig.ignoreVersionMismatch + server.logWithMetadata, + server.newPlatform.__internals.elasticsearch.esNodesCompatibility$ ); - server.expose('waitUntilReady', waitUntilReady); - start(); + + server.expose('waitUntilReady', () => waitUntilHealthy); }, }); } diff --git a/src/legacy/core_plugins/elasticsearch/integration_tests/elasticsearch.test.ts b/src/legacy/core_plugins/elasticsearch/integration_tests/elasticsearch.test.ts new file mode 100644 index 00000000000000..5806c31b784147 --- /dev/null +++ b/src/legacy/core_plugins/elasticsearch/integration_tests/elasticsearch.test.ts @@ -0,0 +1,89 @@ +/* + * 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 { + createTestServers, + TestElasticsearchUtils, + TestKibanaUtils, + TestUtils, + createRootWithCorePlugins, + getKbnServer, +} from '../../../../test_utils/kbn_server'; + +import { BehaviorSubject } from 'rxjs'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { NodesVersionCompatibility } from 'src/core/server/elasticsearch/version_check/ensure_es_version'; + +describe('Elasticsearch plugin', () => { + let servers: TestUtils; + let esServer: TestElasticsearchUtils; + let root: TestKibanaUtils['root']; + let elasticsearch: TestKibanaUtils['kbnServer']['server']['plugins']['elasticsearch']; + + const esNodesCompatibility$ = new BehaviorSubject({ + isCompatible: true, + incompatibleNodes: [], + warningNodes: [], + kibanaVersion: '8.0.0', + }); + + beforeAll(async function() { + const settings = { + elasticsearch: {}, + adjustTimeout: (t: any) => { + jest.setTimeout(t); + }, + }; + servers = createTestServers(settings); + esServer = await servers.startES(); + + const elasticsearchSettings = { + hosts: esServer.hosts, + username: esServer.username, + password: esServer.password, + }; + root = createRootWithCorePlugins({ elasticsearch: elasticsearchSettings }); + + const setup = await root.setup(); + setup.elasticsearch.esNodesCompatibility$ = esNodesCompatibility$; + await root.start(); + + elasticsearch = getKbnServer(root).server.plugins.elasticsearch; + }); + + afterAll(async () => { + await esServer.stop(); + await root.shutdown(); + }, 30000); + + it("should set it's status to green when all nodes are compatible", done => { + jest.setTimeout(30000); + elasticsearch.status.on('green', () => done()); + }); + + it("should set it's status to red when some nodes aren't compatible", done => { + esNodesCompatibility$.next({ + isCompatible: false, + incompatibleNodes: [], + warningNodes: [], + kibanaVersion: '8.0.0', + }); + elasticsearch.status.on('red', () => done()); + }); +}); diff --git a/src/legacy/core_plugins/elasticsearch/server/lib/kibana_version.js b/src/legacy/core_plugins/elasticsearch/lib/version_health_check.js similarity index 57% rename from src/legacy/core_plugins/elasticsearch/server/lib/kibana_version.js rename to src/legacy/core_plugins/elasticsearch/lib/version_health_check.js index 344dbbb5bdf692..4ee8307f490eb0 100644 --- a/src/legacy/core_plugins/elasticsearch/server/lib/kibana_version.js +++ b/src/legacy/core_plugins/elasticsearch/lib/version_health_check.js @@ -17,11 +17,23 @@ * under the License. */ -import { version as kibanaVersion } from '../../../../../../package.json'; +export const versionHealthCheck = (esPlugin, logWithMetadata, esNodesCompatibility$) => { + esPlugin.status.yellow('Waiting for Elasticsearch'); -export default { - // Make the version stubbable to improve testability. - get() { - return kibanaVersion; - }, + return new Promise(resolve => { + esNodesCompatibility$.subscribe(({ isCompatible, message, kibanaVersion, warningNodes }) => { + if (!isCompatible) { + esPlugin.status.red(message); + } else { + if (message) { + logWithMetadata(['warning'], message, { + kibanaVersion, + nodes: warningNodes, + }); + } + esPlugin.status.green('Ready'); + resolve(); + } + }); + }); }; diff --git a/src/legacy/core_plugins/elasticsearch/lib/version_health_check.test.js b/src/legacy/core_plugins/elasticsearch/lib/version_health_check.test.js new file mode 100644 index 00000000000000..ba7c95bcdfec54 --- /dev/null +++ b/src/legacy/core_plugins/elasticsearch/lib/version_health_check.test.js @@ -0,0 +1,71 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { versionHealthCheck } from './version_health_check'; +import { Subject } from 'rxjs'; + +describe('plugins/elasticsearch', () => { + describe('lib/health_version_check', function() { + let plugin; + let logWithMetadata; + + beforeEach(() => { + plugin = { + status: { + red: jest.fn(), + green: jest.fn(), + yellow: jest.fn(), + }, + }; + + logWithMetadata = jest.fn(); + jest.clearAllMocks(); + }); + + it('returned promise resolves when all nodes are compatible ', function() { + const esNodesCompatibility$ = new Subject(); + const versionHealthyPromise = versionHealthCheck( + plugin, + logWithMetadata, + esNodesCompatibility$ + ); + esNodesCompatibility$.next({ isCompatible: true, message: undefined }); + return expect(versionHealthyPromise).resolves.toBe(undefined); + }); + + it('should set elasticsearch plugin status to green when all nodes are compatible', function() { + const esNodesCompatibility$ = new Subject(); + versionHealthCheck(plugin, logWithMetadata, esNodesCompatibility$); + expect(plugin.status.yellow).toHaveBeenCalledWith('Waiting for Elasticsearch'); + expect(plugin.status.green).not.toHaveBeenCalled(); + esNodesCompatibility$.next({ isCompatible: true, message: undefined }); + expect(plugin.status.green).toHaveBeenCalledWith('Ready'); + expect(plugin.status.red).not.toHaveBeenCalled(); + }); + + it('should set elasticsearch plugin status to red when some nodes are incompatible', function() { + const esNodesCompatibility$ = new Subject(); + versionHealthCheck(plugin, logWithMetadata, esNodesCompatibility$); + expect(plugin.status.yellow).toHaveBeenCalledWith('Waiting for Elasticsearch'); + expect(plugin.status.red).not.toHaveBeenCalled(); + esNodesCompatibility$.next({ isCompatible: false, message: 'your nodes are incompatible' }); + expect(plugin.status.red).toHaveBeenCalledWith('your nodes are incompatible'); + expect(plugin.status.green).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/src/legacy/core_plugins/elasticsearch/server/lib/__tests__/ensure_es_version.js b/src/legacy/core_plugins/elasticsearch/server/lib/__tests__/ensure_es_version.js deleted file mode 100644 index 781d198c662364..00000000000000 --- a/src/legacy/core_plugins/elasticsearch/server/lib/__tests__/ensure_es_version.js +++ /dev/null @@ -1,223 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import _ from 'lodash'; -import Bluebird from 'bluebird'; -import sinon from 'sinon'; -import expect from '@kbn/expect'; - -import { esTestConfig } from '@kbn/test'; -import { ensureEsVersion } from '../ensure_es_version'; - -describe('plugins/elasticsearch', () => { - describe('lib/ensure_es_version', () => { - const KIBANA_VERSION = '5.1.0'; - - let server; - - beforeEach(function() { - server = { - log: sinon.stub(), - logWithMetadata: sinon.stub(), - plugins: { - elasticsearch: { - getCluster: sinon - .stub() - .withArgs('admin') - .returns({ callWithInternalUser: sinon.stub() }), - status: { - red: sinon.stub(), - }, - url: esTestConfig.getUrl(), - }, - }, - config() { - return { - get: sinon.stub(), - }; - }, - }; - }); - - function setNodes(/* ...versions */) { - const versions = _.shuffle(arguments); - const nodes = {}; - let i = 0; - - while (versions.length) { - const name = 'node-' + ++i; - const version = versions.shift(); - - const node = { - version: version, - http: { - publish_address: 'http_address', - }, - ip: 'ip', - }; - - if (!_.isString(version)) _.assign(node, version); - nodes[name] = node; - } - - const cluster = server.plugins.elasticsearch.getCluster('admin'); - cluster.callWithInternalUser - .withArgs('nodes.info', sinon.match.any) - .returns(Bluebird.resolve({ nodes: nodes })); - } - - function setNodeWithoutHTTP(version) { - const nodes = { 'node-without-http': { version, ip: 'ip' } }; - const cluster = server.plugins.elasticsearch.getCluster('admin'); - cluster.callWithInternalUser - .withArgs('nodes.info', sinon.match.any) - .returns(Bluebird.resolve({ nodes: nodes })); - } - - it('returns true with single a node that matches', async () => { - setNodes('5.1.0'); - const result = await ensureEsVersion(server, KIBANA_VERSION); - expect(result).to.be(true); - }); - - it('returns true with multiple nodes that satisfy', async () => { - setNodes('5.1.0', '5.2.0', '5.1.1-Beta1'); - const result = await ensureEsVersion(server, KIBANA_VERSION); - expect(result).to.be(true); - }); - - it('throws an error with a single node that is out of date', async () => { - // 5.0.0 ES is too old to work with a 5.1.0 version of Kibana. - setNodes('5.1.0', '5.2.0', '5.0.0'); - try { - await ensureEsVersion(server, KIBANA_VERSION); - } catch (e) { - expect(e).to.be.a(Error); - } - }); - - it('does not throw on outdated nodes, if `ignoreVersionMismatch` is enabled in development mode', async () => { - // set config values - server.config = () => ({ - get: name => { - switch (name) { - case 'env.dev': - return true; - default: - throw new Error(`Unknown option "${name}"`); - } - }, - }); - - // 5.0.0 ES is too old to work with a 5.1.0 version of Kibana. - setNodes('5.1.0', '5.2.0', '5.0.0'); - - const ignoreVersionMismatch = true; - const result = await ensureEsVersion(server, KIBANA_VERSION, ignoreVersionMismatch); - expect(result).to.be(true); - }); - - it('throws an error if `ignoreVersionMismatch` is enabled in production mode', async () => { - // set config values - server.config = () => ({ - get: name => { - switch (name) { - case 'env.dev': - return false; - default: - throw new Error(`Unknown option "${name}"`); - } - }, - }); - - // 5.0.0 ES is too old to work with a 5.1.0 version of Kibana. - setNodes('5.1.0', '5.2.0', '5.0.0'); - - try { - const ignoreVersionMismatch = true; - await ensureEsVersion(server, KIBANA_VERSION, ignoreVersionMismatch); - } catch (e) { - expect(e).to.be.a(Error); - } - }); - - it('fails if that single node is a client node', async () => { - setNodes('5.1.0', '5.2.0', { version: '5.0.0', attributes: { client: 'true' } }); - try { - await ensureEsVersion(server, KIBANA_VERSION); - } catch (e) { - expect(e).to.be.a(Error); - } - }); - - it('warns if a node is only off by a patch version', async () => { - setNodes('5.1.1'); - await ensureEsVersion(server, KIBANA_VERSION); - sinon.assert.callCount(server.logWithMetadata, 2); - expect(server.logWithMetadata.getCall(0).args[0]).to.contain('debug'); - expect(server.logWithMetadata.getCall(1).args[0]).to.contain('warning'); - }); - - it('warns if a node is off by a patch version and without http publish address', async () => { - setNodeWithoutHTTP('5.1.1'); - await ensureEsVersion(server, KIBANA_VERSION); - sinon.assert.callCount(server.logWithMetadata, 2); - expect(server.logWithMetadata.getCall(0).args[0]).to.contain('debug'); - expect(server.logWithMetadata.getCall(1).args[0]).to.contain('warning'); - }); - - it('errors if a node incompatible and without http publish address', async () => { - setNodeWithoutHTTP('6.1.1'); - try { - await ensureEsVersion(server, KIBANA_VERSION); - } catch (e) { - expect(e.message).to.contain('incompatible nodes'); - expect(e).to.be.a(Error); - } - }); - - it('only warns once per node list', async () => { - setNodes('5.1.1'); - - await ensureEsVersion(server, KIBANA_VERSION); - sinon.assert.callCount(server.logWithMetadata, 2); - expect(server.logWithMetadata.getCall(0).args[0]).to.contain('debug'); - expect(server.logWithMetadata.getCall(1).args[0]).to.contain('warning'); - - await ensureEsVersion(server, KIBANA_VERSION); - sinon.assert.callCount(server.logWithMetadata, 3); - expect(server.logWithMetadata.getCall(2).args[0]).to.contain('debug'); - }); - - it('warns again if the node list changes', async () => { - setNodes('5.1.1'); - - await ensureEsVersion(server, KIBANA_VERSION); - sinon.assert.callCount(server.logWithMetadata, 2); - expect(server.logWithMetadata.getCall(0).args[0]).to.contain('debug'); - expect(server.logWithMetadata.getCall(1).args[0]).to.contain('warning'); - - setNodes('5.1.2'); - await ensureEsVersion(server, KIBANA_VERSION); - sinon.assert.callCount(server.logWithMetadata, 4); - expect(server.logWithMetadata.getCall(2).args[0]).to.contain('debug'); - expect(server.logWithMetadata.getCall(3).args[0]).to.contain('warning'); - }); - }); -}); diff --git a/src/legacy/core_plugins/elasticsearch/server/lib/__tests__/health_check.js b/src/legacy/core_plugins/elasticsearch/server/lib/__tests__/health_check.js deleted file mode 100644 index 3b593c6352394e..00000000000000 --- a/src/legacy/core_plugins/elasticsearch/server/lib/__tests__/health_check.js +++ /dev/null @@ -1,151 +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 Bluebird from 'bluebird'; -import sinon from 'sinon'; -import expect from '@kbn/expect'; - -const NoConnections = require('elasticsearch').errors.NoConnections; - -import healthCheck from '../health_check'; -import kibanaVersion from '../kibana_version'; - -const esPort = 9220; - -describe('plugins/elasticsearch', () => { - describe('lib/health_check', function() { - let health; - let plugin; - let cluster; - let server; - const sandbox = sinon.createSandbox(); - - function getTimerCount() { - return Object.keys(sandbox.clock.timers || {}).length; - } - - beforeEach(() => { - sandbox.useFakeTimers(); - const COMPATIBLE_VERSION_NUMBER = '5.0.0'; - - // Stub the Kibana version instead of drawing from package.json. - sandbox.stub(kibanaVersion, 'get').returns(COMPATIBLE_VERSION_NUMBER); - - // setup the plugin stub - plugin = { - name: 'elasticsearch', - status: { - red: sinon.stub(), - green: sinon.stub(), - yellow: sinon.stub(), - }, - }; - - cluster = { callWithInternalUser: sinon.stub(), errors: { NoConnections } }; - cluster.callWithInternalUser.withArgs('index', sinon.match.any).returns(Bluebird.resolve()); - cluster.callWithInternalUser - .withArgs('mget', sinon.match.any) - .returns(Bluebird.resolve({ ok: true })); - cluster.callWithInternalUser - .withArgs('get', sinon.match.any) - .returns(Bluebird.resolve({ found: false })); - cluster.callWithInternalUser - .withArgs('search', sinon.match.any) - .returns(Bluebird.resolve({ hits: { hits: [] } })); - cluster.callWithInternalUser.withArgs('nodes.info', sinon.match.any).returns( - Bluebird.resolve({ - nodes: { - 'node-01': { - version: COMPATIBLE_VERSION_NUMBER, - http_address: `inet[/127.0.0.1:${esPort}]`, - ip: '127.0.0.1', - }, - }, - }) - ); - - // Setup the server mock - server = { - logWithMetadata: sinon.stub(), - info: { port: 5601 }, - config: () => ({ get: sinon.stub() }), - plugins: { - elasticsearch: { - getCluster: sinon.stub().returns(cluster), - }, - }, - ext: sinon.stub(), - }; - - health = healthCheck(plugin, server, 0); - }); - - afterEach(() => sandbox.restore()); - - it('should stop when cluster is shutdown', () => { - // ensure that health.start() is responsible for the timer we are observing - expect(getTimerCount()).to.be(0); - health.start(); - expect(getTimerCount()).to.be(1); - - // ensure that a server extension was registered - sinon.assert.calledOnce(server.ext); - sinon.assert.calledWithExactly(server.ext, sinon.match.string, sinon.match.func); - - const [, handler] = server.ext.firstCall.args; - handler(); // this should be health.stop - - // ensure that the handler unregistered the timer - expect(getTimerCount()).to.be(0); - }); - - it('should set the cluster green if everything is ready', function() { - cluster.callWithInternalUser.withArgs('ping').returns(Bluebird.resolve()); - - return health.run().then(function() { - sinon.assert.calledOnce(plugin.status.yellow); - sinon.assert.calledWithExactly(plugin.status.yellow, 'Waiting for Elasticsearch'); - - sinon.assert.calledOnce( - cluster.callWithInternalUser.withArgs('nodes.info', sinon.match.any) - ); - sinon.assert.notCalled(plugin.status.red); - sinon.assert.calledOnce(plugin.status.green); - sinon.assert.calledWithExactly(plugin.status.green, 'Ready'); - }); - }); - - describe('#waitUntilReady', function() { - it('waits for green status', function() { - plugin.status.once = sinon.spy(function(event, handler) { - expect(event).to.be('green'); - setImmediate(handler); - }); - - const waitUntilReadyPromise = health.waitUntilReady(); - - sandbox.clock.runAll(); - - return waitUntilReadyPromise.then(function() { - sinon.assert.calledOnce(plugin.status.once); - }); - }); - }); - }); -}); diff --git a/src/legacy/core_plugins/elasticsearch/server/lib/ensure_es_version.js b/src/legacy/core_plugins/elasticsearch/server/lib/ensure_es_version.js deleted file mode 100644 index 8d304cd5584182..00000000000000 --- a/src/legacy/core_plugins/elasticsearch/server/lib/ensure_es_version.js +++ /dev/null @@ -1,126 +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. - */ - -/** - * ES and Kibana versions are locked, so Kibana should require that ES has the same version as - * that defined in Kibana's package.json. - */ - -import { forEach, get } from 'lodash'; -import { coerce } from 'semver'; -import isEsCompatibleWithKibana from './is_es_compatible_with_kibana'; - -/** - * tracks the node descriptions that get logged in warnings so - * that we don't spam the log with the same message over and over. - * - * There are situations, like in testing or multi-tenancy, where - * the server argument changes, so we must track the previous - * node warnings per server - */ -const lastWarnedNodesForServer = new WeakMap(); - -export function ensureEsVersion(server, kibanaVersion, ignoreVersionMismatch = false) { - const { callWithInternalUser } = server.plugins.elasticsearch.getCluster('admin'); - - server.logWithMetadata(['plugin', 'debug'], 'Checking Elasticsearch version'); - return callWithInternalUser('nodes.info', { - filterPath: ['nodes.*.version', 'nodes.*.http.publish_address', 'nodes.*.ip'], - }).then(function(info) { - // Aggregate incompatible ES nodes. - const incompatibleNodes = []; - - // Aggregate ES nodes which should prompt a Kibana upgrade. - const warningNodes = []; - - forEach(info.nodes, esNode => { - if (!isEsCompatibleWithKibana(esNode.version, kibanaVersion)) { - // Exit early to avoid collecting ES nodes with newer major versions in the `warningNodes`. - return incompatibleNodes.push(esNode); - } - - // It's acceptable if ES and Kibana versions are not the same so long as - // they are not incompatible, but we should warn about it - - // Ignore version qualifiers - // https://github.com/elastic/elasticsearch/issues/36859 - const looseMismatch = coerce(esNode.version).version !== coerce(kibanaVersion).version; - if (looseMismatch) { - warningNodes.push(esNode); - } - }); - - function getHumanizedNodeNames(nodes) { - return nodes.map(node => { - const publishAddress = get(node, 'http.publish_address') - ? get(node, 'http.publish_address') + ' ' - : ''; - return 'v' + node.version + ' @ ' + publishAddress + '(' + node.ip + ')'; - }); - } - - if (warningNodes.length) { - const simplifiedNodes = warningNodes.map(node => ({ - version: node.version, - http: { - publish_address: get(node, 'http.publish_address'), - }, - ip: node.ip, - })); - - // Don't show the same warning over and over again. - const warningNodeNames = getHumanizedNodeNames(simplifiedNodes).join(', '); - if (lastWarnedNodesForServer.get(server) !== warningNodeNames) { - lastWarnedNodesForServer.set(server, warningNodeNames); - server.logWithMetadata( - ['warning'], - `You're running Kibana ${kibanaVersion} with some different versions of ` + - 'Elasticsearch. Update Kibana or Elasticsearch to the same ' + - `version to prevent compatibility issues: ${warningNodeNames}`, - { - kibanaVersion, - nodes: simplifiedNodes, - } - ); - } - } - - if (incompatibleNodes.length && !shouldIgnoreVersionMismatch(server, ignoreVersionMismatch)) { - const incompatibleNodeNames = getHumanizedNodeNames(incompatibleNodes); - throw new Error( - `This version of Kibana requires Elasticsearch v` + - `${kibanaVersion} on all nodes. I found ` + - `the following incompatible nodes in your cluster: ${incompatibleNodeNames.join(', ')}` - ); - } - - return true; - }); -} - -function shouldIgnoreVersionMismatch(server, ignoreVersionMismatch) { - const isDevMode = server.config().get('env.dev'); - if (!isDevMode && ignoreVersionMismatch) { - throw new Error( - `Option "elasticsearch.ignoreVersionMismatch" can only be used in development mode` - ); - } - - return isDevMode && ignoreVersionMismatch; -} diff --git a/src/legacy/core_plugins/elasticsearch/server/lib/health_check.js b/src/legacy/core_plugins/elasticsearch/server/lib/health_check.js deleted file mode 100644 index 40053ec7745421..00000000000000 --- a/src/legacy/core_plugins/elasticsearch/server/lib/health_check.js +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import Bluebird from 'bluebird'; -import kibanaVersion from './kibana_version'; -import { ensureEsVersion } from './ensure_es_version'; - -export default function(plugin, server, requestDelay, ignoreVersionMismatch) { - plugin.status.yellow('Waiting for Elasticsearch'); - - function waitUntilReady() { - return new Bluebird(resolve => { - plugin.status.once('green', resolve); - }); - } - - function check() { - return ensureEsVersion(server, kibanaVersion.get(), ignoreVersionMismatch) - .then(() => plugin.status.green('Ready')) - .catch(err => plugin.status.red(err)); - } - - let timeoutId = null; - - function scheduleCheck(ms) { - if (timeoutId) return; - - const myId = setTimeout(function() { - check().finally(function() { - if (timeoutId === myId) startorRestartChecking(); - }); - }, ms); - - timeoutId = myId; - } - - function startorRestartChecking() { - scheduleCheck(stopChecking() ? requestDelay : 1); - } - - function stopChecking() { - if (!timeoutId) return false; - clearTimeout(timeoutId); - timeoutId = null; - return true; - } - - server.ext('onPreStop', stopChecking); - - return { - waitUntilReady: waitUntilReady, - run: check, - start: startorRestartChecking, - stop: stopChecking, - isRunning: function() { - return !!timeoutId; - }, - }; -} diff --git a/src/legacy/server/http/integration_tests/default_route_provider.test.ts b/src/legacy/server/http/integration_tests/default_route_provider.test.ts index 4898cb2b67852e..d91438d904558b 100644 --- a/src/legacy/server/http/integration_tests/default_route_provider.test.ts +++ b/src/legacy/server/http/integration_tests/default_route_provider.test.ts @@ -29,7 +29,7 @@ let mockDefaultRouteSetting: any = ''; describe('default route provider', () => { let root: Root; beforeAll(async () => { - root = kbnTestServer.createRoot(); + root = kbnTestServer.createRoot({ migrations: { skip: true } }); await root.setup(); await root.start(); diff --git a/src/legacy/server/http/integration_tests/default_route_provider_config.test.ts b/src/legacy/server/http/integration_tests/default_route_provider_config.test.ts index da785a59893ab6..8365941cbeb10e 100644 --- a/src/legacy/server/http/integration_tests/default_route_provider_config.test.ts +++ b/src/legacy/server/http/integration_tests/default_route_provider_config.test.ts @@ -30,6 +30,7 @@ describe('default route provider', () => { server: { defaultRoute: '/app/some/default/route', }, + migrations: { skip: true }, }); await root.setup(); diff --git a/src/legacy/server/http/integration_tests/max_payload_size.test.js b/src/legacy/server/http/integration_tests/max_payload_size.test.js index 4408f0141bb21a..7f22f83c78f0ee 100644 --- a/src/legacy/server/http/integration_tests/max_payload_size.test.js +++ b/src/legacy/server/http/integration_tests/max_payload_size.test.js @@ -21,7 +21,7 @@ import * as kbnTestServer from '../../../../test_utils/kbn_server'; let root; beforeAll(async () => { - root = kbnTestServer.createRoot({ server: { maxPayloadBytes: 100 } }); + root = kbnTestServer.createRoot({ server: { maxPayloadBytes: 100 }, migrations: { skip: true } }); await root.setup(); await root.start(); diff --git a/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts b/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts index 776275715921be..92be88b91c6523 100644 --- a/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts +++ b/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts @@ -108,7 +108,10 @@ describe('onPostAuthInterceptor', () => { availableSpaces: any[], testOptions = { simulateGetSpacesFailure: false, simulateGetSingleSpaceFailure: false } ) { - const { http } = await root.setup(); + const { http, elasticsearch } = await root.setup(); + + // Mock esNodesCompatibility$ to prevent `root.start()` from blocking on ES version check + elasticsearch.esNodesCompatibility$ = elasticsearchServiceMock.createInternalSetup().esNodesCompatibility$; const loggingMock = loggingServiceMock .create() diff --git a/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.test.ts b/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.test.ts index d6ff4a20052e41..5e6cf67ee8c907 100644 --- a/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.test.ts +++ b/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.test.ts @@ -17,6 +17,7 @@ import { import * as kbnTestServer from '../../../../../../src/test_utils/kbn_server'; import { LegacyAPI } from '../../plugin'; +import { elasticsearchServiceMock } from 'src/core/server/mocks'; describe('onRequestInterceptor', () => { let root: ReturnType; @@ -104,7 +105,9 @@ describe('onRequestInterceptor', () => { routes: 'legacy' | 'new-platform'; } async function setup(opts: SetupOpts = { basePath: '/', routes: 'legacy' }) { - const { http } = await root.setup(); + const { http, elasticsearch } = await root.setup(); + // Mock esNodesCompatibility$ to prevent `root.start()` from blocking on ES version check + elasticsearch.esNodesCompatibility$ = elasticsearchServiceMock.createInternalSetup().esNodesCompatibility$; initSpacesOnRequestInterceptor({ getLegacyAPI: () => From 2260e7cdbe2da24bfa13866fc37bcdbc8793dd74 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Fri, 31 Jan 2020 18:10:34 +0100 Subject: [PATCH 06/33] [Uptime] Added location overview in monitor in status column (#55720) * Reintroduce a column for url. * Reintroduce original URL column. * added location overview column * fix types * update snaps * Update busted test snapshots. * Update x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_status_column.tsx Co-Authored-By: Justin Kambic * update test * rename * update mobile layout * update column laytout * fixed i18n * remove icon Co-authored-by: Justin Kambic Co-authored-by: Elastic Machine --- .../uptime/common/constants/constants.ts | 7 - .../plugins/uptime/common/constants/index.ts | 2 +- .../plugins/uptime/common/constants/ui.ts | 33 +++ .../monitor_bar_series.test.tsx.snap | 1 + .../functional/charts/monitor_bar_series.tsx | 2 +- .../location_map/location_status_tags.tsx | 24 +- .../__snapshots__/monitor_list.test.tsx.snap | 26 +- .../monitor_list_pagination.test.tsx.snap | 24 +- .../monitor_list_status_column.test.tsx.snap | 239 ++++++++++++++--- .../monitor_list_status_column.test.tsx | 241 +++++++++++++++++- .../functional/monitor_list/monitor_list.tsx | 31 ++- .../monitor_status_list.tsx | 13 +- .../monitor_status_row.tsx | 7 +- .../monitor_list_status_column.tsx | 127 +++++++-- .../functional/monitor_list/translations.ts | 8 + 15 files changed, 662 insertions(+), 123 deletions(-) delete mode 100644 x-pack/legacy/plugins/uptime/common/constants/constants.ts create mode 100644 x-pack/legacy/plugins/uptime/common/constants/ui.ts diff --git a/x-pack/legacy/plugins/uptime/common/constants/constants.ts b/x-pack/legacy/plugins/uptime/common/constants/constants.ts deleted file mode 100644 index 0c35bc97344864..00000000000000 --- a/x-pack/legacy/plugins/uptime/common/constants/constants.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export const UNNAMED_LOCATION = 'Unnamed-location'; diff --git a/x-pack/legacy/plugins/uptime/common/constants/index.ts b/x-pack/legacy/plugins/uptime/common/constants/index.ts index 0a95960825f022..9d5ad4607491c3 100644 --- a/x-pack/legacy/plugins/uptime/common/constants/index.ts +++ b/x-pack/legacy/plugins/uptime/common/constants/index.ts @@ -11,4 +11,4 @@ export { INDEX_NAMES } from './index_names'; export * from './capabilities'; export { PLUGIN } from './plugin'; export { QUERY, STATES } from './query'; -export * from './constants'; +export * from './ui'; diff --git a/x-pack/legacy/plugins/uptime/common/constants/ui.ts b/x-pack/legacy/plugins/uptime/common/constants/ui.ts new file mode 100644 index 00000000000000..c91a2f66251942 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/common/constants/ui.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export enum STATUS { + UP = 'up', + DOWN = 'down', +} + +export const UNNAMED_LOCATION = 'Unnamed-location'; + +export const SHORT_TS_LOCALE = 'en-short-locale'; + +export const SHORT_TIMESPAN_LOCALE = { + relativeTime: { + future: 'in %s', + past: '%s ago', + s: '%ds', + ss: '%ss', + m: '%dm', + mm: '%dm', + h: '%dh', + hh: '%dh', + d: '%dd', + dd: '%dd', + M: '%d Mon', + MM: '%d Mon', + y: '%d Yr', + yy: '%d Yr', + }, +}; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/monitor_bar_series.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/monitor_bar_series.test.tsx.snap index 78b2bfdecb87af..c3b99c9785cbe0 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/monitor_bar_series.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/monitor_bar_series.test.tsx.snap @@ -5,6 +5,7 @@ exports[`MonitorBarSeries component renders a series when there are down items 1 style={ Object { "height": 50, + "marginRight": 15, "maxWidth": "1200px", "width": "100%", } diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/monitor_bar_series.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/charts/monitor_bar_series.tsx index a0cbdc59221237..ce91bf5b1638f4 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/charts/monitor_bar_series.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/charts/monitor_bar_series.tsx @@ -53,7 +53,7 @@ export const MonitorBarSeries = ({ const id = 'downSeries'; return seriesHasDownValues(histogramSeries) ? ( -
+
{ const prevLocal: string = moment.locale() ?? 'en'; const renderTags = () => { - moment.defineLocale('en-tag', { - relativeTime: { - future: 'in %s', - past: '%s ago', - s: '%ds', - ss: '%ss', - m: '%dm', - mm: '%dm', - h: '%dh', - hh: '%dh', - d: '%dd', - dd: '%dd', - M: '%d Mon', - MM: '%d Mon', - y: '%d Yr', - yy: '%d Yr', - }, - }); + const shortLocale = moment.locale(SHORT_TS_LOCALE) === SHORT_TS_LOCALE; + if (!shortLocale) { + moment.defineLocale(SHORT_TS_LOCALE, SHORT_TIMESPAN_LOCALE); + } + const tags = ( {downLocations.map((item, ind) => tagLabel(item, ind, danger))} diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap index 1de49f12236992..f779efca7b18a7 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap @@ -24,20 +24,24 @@ exports[`MonitorList component renders a no items message when no data is provid Object { "align": "left", "field": "state.monitor.status", + "mobileOptions": Object { + "fullWidth": true, + }, "name": "Status", "render": [Function], - "width": "20%", }, Object { "align": "left", "field": "state.monitor.name", + "mobileOptions": Object { + "fullWidth": true, + }, "name": "Name", "render": [Function], "sortable": true, - "width": "30%", }, Object { - "aligh": "left", + "align": "left", "field": "state.url.full", "name": "Url", "render": [Function], @@ -58,6 +62,7 @@ exports[`MonitorList component renders a no items message when no data is provid "name": "", "render": [Function], "sortable": true, + "width": "24px", }, ] } @@ -83,6 +88,7 @@ exports[`MonitorList component renders a no items message when no data is provid @@ -122,20 +129,24 @@ exports[`MonitorList component renders the monitor list 1`] = ` Object { "align": "left", "field": "state.monitor.status", + "mobileOptions": Object { + "fullWidth": true, + }, "name": "Status", "render": [Function], - "width": "20%", }, Object { "align": "left", "field": "state.monitor.name", + "mobileOptions": Object { + "fullWidth": true, + }, "name": "Name", "render": [Function], "sortable": true, - "width": "30%", }, Object { - "aligh": "left", + "align": "left", "field": "state.url.full", "name": "Url", "render": [Function], @@ -156,6 +167,7 @@ exports[`MonitorList component renders the monitor list 1`] = ` "name": "", "render": [Function], "sortable": true, + "width": "24px", }, ] } @@ -243,6 +255,7 @@ exports[`MonitorList component renders the monitor list 1`] = ` diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_list_pagination.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_list_pagination.test.tsx.snap index aa9d3a3ed0d8c8..03a5a15eea1a47 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_list_pagination.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_list_pagination.test.tsx.snap @@ -24,20 +24,24 @@ exports[`MonitorList component renders a no items message when no data is provid Object { "align": "left", "field": "state.monitor.status", + "mobileOptions": Object { + "fullWidth": true, + }, "name": "Status", "render": [Function], - "width": "20%", }, Object { "align": "left", "field": "state.monitor.name", + "mobileOptions": Object { + "fullWidth": true, + }, "name": "Name", "render": [Function], "sortable": true, - "width": "30%", }, Object { - "aligh": "left", + "align": "left", "field": "state.url.full", "name": "Url", "render": [Function], @@ -58,6 +62,7 @@ exports[`MonitorList component renders a no items message when no data is provid "name": "", "render": [Function], "sortable": true, + "width": "24px", }, ] } @@ -83,6 +88,7 @@ exports[`MonitorList component renders a no items message when no data is provid @@ -122,20 +129,24 @@ exports[`MonitorList component renders the monitor list 1`] = ` Object { "align": "left", "field": "state.monitor.status", + "mobileOptions": Object { + "fullWidth": true, + }, "name": "Status", "render": [Function], - "width": "20%", }, Object { "align": "left", "field": "state.monitor.name", + "mobileOptions": Object { + "fullWidth": true, + }, "name": "Name", "render": [Function], "sortable": true, - "width": "30%", }, Object { - "aligh": "left", + "align": "left", "field": "state.url.full", "name": "Url", "render": [Function], @@ -156,6 +167,7 @@ exports[`MonitorList component renders the monitor list 1`] = ` "name": "", "render": [Function], "sortable": true, + "width": "24px", }, ] } diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_list_status_column.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_list_status_column.test.tsx.snap index 69a0616a7bed8b..f3b90fbf6ee913 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_list_status_column.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_list_status_column.test.tsx.snap @@ -1,11 +1,20 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`MonitorListStatusColumn can handle a non-numeric timestamp value 1`] = ` - - + Up - + + Thu May 09 2019 10:15:11 GMT-0400 + + } + delay="regular" + position="top" + > - Thu May 09 2019 10:15:11 GMT-0400 + a few seconds ago - } - delay="regular" - position="top" + + + + + - - a few seconds ago - - + in 0/0 Location + - + `; exports[`MonitorListStatusColumn provides expected tooltip and display times 1`] = ` - - + Up - + + Thu May 09 2019 10:15:11 GMT-0400 + + } + delay="regular" + position="top" + > - Thu May 09 2019 10:15:11 GMT-0400 + a few seconds ago + + + + + + in 0/0 Location + + + +`; + +exports[`MonitorListStatusColumn will display location status 1`] = ` + + + - + + + Thu May 09 2019 10:15:11 GMT-0400 + + } + delay="regular" + position="top" > - a few seconds ago - - + + a few seconds ago + + + + + + + in 1/3 Locations + - + +`; + +exports[`MonitorListStatusColumn will render display location status 1`] = ` +.c1 { + padding-left: 17px; +} + +@media (max-width:574px) { + .c0 { + min-width: 230px; + } +} + +
+
+
+
+
+ +
+
+ Up +
+
+
+ + +
+
+ a few seconds ago +
+
+
+
+
+
+
+ in 1/3 Locations +
+
+
`; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/monitor_list_status_column.test.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/monitor_list_status_column.test.tsx index 2a834377fee8e3..406e18535f34c3 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/monitor_list_status_column.test.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/monitor_list_status_column.test.tsx @@ -6,8 +6,10 @@ import React from 'react'; import moment from 'moment'; -import { shallowWithIntl } from 'test_utils/enzyme_helpers'; -import { MonitorListStatusColumn } from '../monitor_list_status_column'; +import { renderWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; +import { getLocationStatus, MonitorListStatusColumn } from '../monitor_list_status_column'; +import { Check } from '../../../../../common/graphql/types'; +import { STATUS } from '../../../../../common/constants'; describe('MonitorListStatusColumn', () => { beforeAll(() => { @@ -18,15 +20,246 @@ describe('MonitorListStatusColumn', () => { Date.prototype.toString = jest.fn(() => 'Tue, 01 Jan 2019 00:00:00 GMT'); }); + let upChecks: Check[]; + + let downChecks: Check[]; + + let checks: Check[]; + + beforeEach(() => { + upChecks = [ + { + agent: { id: '6a2f2a1c-e346-49ed-8418-6d48af8841d6' }, + container: null, + kubernetes: null, + monitor: { + ip: '104.86.46.103', + name: '', + status: 'up', + }, + observer: { + geo: { + name: 'Berlin', + location: { + lat: 40.73060997761786, + lon: -73.93524203449488, + }, + }, + }, + timestamp: '1579794631464', + }, + { + agent: { id: '1117fd01-bc1a-4aa5-bfab-40ab455eadf9' }, + container: null, + kubernetes: null, + monitor: { + ip: '104.86.46.103', + name: '', + status: 'up', + }, + observer: { + geo: { + name: 'Islamabad', + location: { + lat: 40.73060997761786, + lon: -73.93524203449488, + }, + }, + }, + timestamp: '1579794634220', + }, + { + agent: { id: 'eda59510-45e8-4dfe-b0f8-959c449e3565' }, + container: null, + kubernetes: null, + monitor: { + ip: '104.86.46.103', + name: '', + status: 'up', + }, + observer: { + geo: { + name: 'st-paul', + location: { + lat: 52.48744798824191, + lon: 13.394797928631306, + }, + }, + }, + timestamp: '1579794628368', + }, + ]; + + downChecks = [ + { + agent: { id: '6a2f2a1c-e346-49ed-8418-6d48af8841d6' }, + container: null, + kubernetes: null, + monitor: { + ip: '104.86.46.103', + name: '', + status: 'down', + }, + observer: { + geo: { + name: 'Berlin', + location: { + lat: 40.73060997761786, + lon: -73.93524203449488, + }, + }, + }, + timestamp: '1579794631464', + }, + { + agent: { id: '1117fd01-bc1a-4aa5-bfab-40ab455eadf9' }, + container: null, + kubernetes: null, + monitor: { + ip: '104.86.46.103', + name: '', + status: 'down', + }, + observer: { + geo: { + name: 'Islamabad', + location: { + lat: 40.73060997761786, + lon: -73.93524203449488, + }, + }, + }, + timestamp: '1579794634220', + }, + { + agent: { id: 'eda59510-45e8-4dfe-b0f8-959c449e3565' }, + container: null, + kubernetes: null, + monitor: { + ip: '104.86.46.103', + name: '', + status: 'down', + }, + observer: { + geo: { + name: 'st-paul', + location: { + lat: 52.48744798824191, + lon: 13.394797928631306, + }, + }, + }, + timestamp: '1579794628368', + }, + ]; + + checks = [ + { + agent: { id: '6a2f2a1c-e346-49ed-8418-6d48af8841d6' }, + container: null, + kubernetes: null, + monitor: { + ip: '104.86.46.103', + name: '', + status: 'up', + }, + observer: { + geo: { + name: 'Berlin', + location: { + lat: 40.73060997761786, + lon: -73.93524203449488, + }, + }, + }, + timestamp: '1579794631464', + }, + { + agent: { id: '1117fd01-bc1a-4aa5-bfab-40ab455eadf9' }, + container: null, + kubernetes: null, + monitor: { + ip: '104.86.46.103', + name: '', + status: 'down', + }, + observer: { + geo: { + name: 'Islamabad', + location: { + lat: 40.73060997761786, + lon: -73.93524203449488, + }, + }, + }, + timestamp: '1579794634220', + }, + { + agent: { id: 'eda59510-45e8-4dfe-b0f8-959c449e3565' }, + container: null, + kubernetes: null, + monitor: { + ip: '104.86.46.103', + name: '', + status: 'down', + }, + observer: { + geo: { + name: 'st-paul', + location: { + lat: 52.48744798824191, + lon: 13.394797928631306, + }, + }, + }, + timestamp: '1579794628368', + }, + ]; + }); + it('provides expected tooltip and display times', () => { - const component = shallowWithIntl(); + const component = shallowWithIntl( + + ); expect(component).toMatchSnapshot(); }); it('can handle a non-numeric timestamp value', () => { const component = shallowWithIntl( - + + ); + expect(component).toMatchSnapshot(); + }); + + it('will display location status', () => { + const component = shallowWithIntl( + ); expect(component).toMatchSnapshot(); }); + + it('will render display location status', () => { + const component = renderWithIntl( + + ); + expect(component).toMatchSnapshot(); + }); + + it(' will test getLocationStatus location', () => { + let statusMessage = getLocationStatus(checks, STATUS.UP); + + expect(statusMessage).toBe('in 1/3 Locations'); + + statusMessage = getLocationStatus(checks, STATUS.DOWN); + + expect(statusMessage).toBe('in 2/3 Locations'); + + statusMessage = getLocationStatus(upChecks, STATUS.UP); + + expect(statusMessage).toBe('in 3/3 Locations'); + + statusMessage = getLocationStatus(downChecks, STATUS.UP); + + expect(statusMessage).toBe('in 0/3 Locations'); + }); }); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list.tsx index 1d0930f1faaefc..c8385440a7d49a 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list.tsx @@ -16,8 +16,7 @@ import { EuiTitle, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { get } from 'lodash'; -import React, { useState, Fragment } from 'react'; +import React, { useState } from 'react'; import styled from 'styled-components'; import { withUptimeGraphQL, UptimeGraphQLQueryProps } from '../../higher_order'; import { monitorStatesQuery } from '../../../queries/monitor_states_query'; @@ -68,10 +67,11 @@ export const MonitorListComponent = (props: Props) => { loading, } = props; const [drawerIds, updateDrawerIds] = useState([]); - const items = get(data, 'monitorStates.summaries', []); - const nextPagePagination = get(data, 'monitorStates.nextPagePagination'); - const prevPagePagination = get(data, 'monitorStates.prevPagePagination'); + const items = data?.monitorStates?.summaries ?? []; + + const nextPagePagination = data?.monitorStates?.nextPagePagination ?? ''; + const prevPagePagination = data?.monitorStates?.prevPagePagination ?? ''; const getExpandedRowMap = () => { return drawerIds.reduce((map: ExpandedRowMap, id: string) => { @@ -89,18 +89,24 @@ export const MonitorListComponent = (props: Props) => { const columns = [ { align: 'left' as const, - width: '20%', field: 'state.monitor.status', name: labels.STATUS_COLUMN_LABEL, - render: (status: string, { state: { timestamp } }: MonitorSummary) => { - return ; + mobileOptions: { + fullWidth: true, + }, + render: (status: string, { state: { timestamp, checks } }: MonitorSummary) => { + return ( + + ); }, }, { align: 'left' as const, - width: '30%', field: 'state.monitor.name', name: labels.NAME_COLUMN_LABEL, + mobileOptions: { + fullWidth: true, + }, render: (name: string, summary: MonitorSummary) => ( {name ? name : `Unnamed - ${summary.monitor_id}`} @@ -109,7 +115,7 @@ export const MonitorListComponent = (props: Props) => { sortable: true, }, { - aligh: 'left' as const, + align: 'left' as const, field: 'state.url.full', name: labels.URL, render: (url: string, summary: MonitorSummary) => ( @@ -140,6 +146,7 @@ export const MonitorListComponent = (props: Props) => { name: '', sortable: true, isExpander: true, + width: '24px', render: (id: string) => { return ( { ]; return ( - + <>
@@ -211,7 +218,7 @@ export const MonitorListComponent = (props: Props) => { - + ); }; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/monitor_status_list.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/monitor_status_list.tsx index 04c5dc7d713714..a2042e379dd80f 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/monitor_status_list.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/monitor_status_list.tsx @@ -11,7 +11,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { Check } from '../../../../../common/graphql/types'; import { LocationLink } from './location_link'; import { MonitorStatusRow } from './monitor_status_row'; -import { UNNAMED_LOCATION } from '../../../../../common/constants'; +import { STATUS, UNNAMED_LOCATION } from '../../../../../common/constants'; interface MonitorStatusListProps { /** @@ -20,9 +20,6 @@ interface MonitorStatusListProps { checks: Check[]; } -export const UP = 'up'; -export const DOWN = 'down'; - export const MonitorStatusList = ({ checks }: MonitorStatusListProps) => { const upChecks: Set = new Set(); const downChecks: Set = new Set(); @@ -31,9 +28,9 @@ export const MonitorStatusList = ({ checks }: MonitorStatusListProps) => { // Doing this way because name is either string or null, get() default value only works on undefined value const location = get(check, 'observer.geo.name', null) || UNNAMED_LOCATION; - if (check.monitor.status === UP) { + if (check.monitor.status === STATUS.UP) { upChecks.add(capitalize(location)); - } else if (check.monitor.status === DOWN) { + } else if (check.monitor.status === STATUS.DOWN) { downChecks.add(capitalize(location)); } }); @@ -43,8 +40,8 @@ export const MonitorStatusList = ({ checks }: MonitorStatusListProps) => { return ( <> - - + + {(downChecks.has(UNNAMED_LOCATION) || upChecks.has(UNNAMED_LOCATION)) && ( <> diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/monitor_status_row.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/monitor_status_row.tsx index e724986c2505e2..50028e1ddea180 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/monitor_status_row.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/monitor_status_row.tsx @@ -8,8 +8,7 @@ import React, { useContext } from 'react'; import { EuiHealth, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { UptimeThemeContext } from '../../../../contexts'; -import { UP } from './monitor_status_list'; -import { UNNAMED_LOCATION } from '../../../../../common/constants'; +import { UNNAMED_LOCATION, STATUS } from '../../../../../common/constants'; interface MonitorStatusRowProps { /** @@ -27,7 +26,7 @@ export const MonitorStatusRow = ({ locationNames, status }: MonitorStatusRowProp colors: { success, danger }, } = useContext(UptimeThemeContext); - const color = status === UP ? success : danger; + const color = status === STATUS.UP ? success : danger; let checkListArray = [...locationNames]; // If un-named location exists, move it to end @@ -44,7 +43,7 @@ export const MonitorStatusRow = ({ locationNames, status }: MonitorStatusRowProp return ( <> - {status === UP ? ( + {status === STATUS.UP ? ( { switch (status) { - case 'up': + case STATUS.UP: return 'success'; - case 'down': + case STATUS.DOWN: return 'danger'; default: return ''; @@ -27,42 +50,98 @@ const getHealthColor = (status: string): string => { const getHealthMessage = (status: string): string | null => { switch (status) { - case 'up': - return i18n.translate('xpack.uptime.monitorList.statusColumn.upLabel', { - defaultMessage: 'Up', - }); - case 'down': - return i18n.translate('xpack.uptime.monitorList.statusColumn.downLabel', { - defaultMessage: 'Down', - }); + case STATUS.UP: + return labels.UP; + case STATUS.DOWN: + return labels.DOWN; default: return null; } }; +const getRelativeShortTimeStamp = (timeStamp: any) => { + const prevLocale: string = moment.locale() ?? 'en'; + + const shortLocale = moment.locale(SHORT_TS_LOCALE) === SHORT_TS_LOCALE; + + if (!shortLocale) { + moment.defineLocale(SHORT_TS_LOCALE, SHORT_TIMESPAN_LOCALE); + } + + const shortTimestamp = parseTimestamp(timeStamp).fromNow(); + + // Reset it so, it does't impact other part of the app + moment.locale(prevLocale); + return shortTimestamp; +}; + +export const getLocationStatus = (checks: Check[], status: string) => { + const upChecks: Set = new Set(); + const downChecks: Set = new Set(); + + checks.forEach((check: Check) => { + const location = check?.observer?.geo?.name ?? UNNAMED_LOCATION; + + if (check.monitor.status === STATUS.UP) { + upChecks.add(capitalize(location)); + } else if (check.monitor.status === STATUS.DOWN) { + downChecks.add(capitalize(location)); + } + }); + + // if monitor is down in one dns, it will be considered down so removing it from up list + const absUpChecks: Set = new Set([...upChecks].filter(item => !downChecks.has(item))); + + const totalLocations = absUpChecks.size + downChecks.size; + let statusMessage = ''; + if (status === STATUS.DOWN) { + statusMessage = `${downChecks.size}/${totalLocations}`; + } else { + statusMessage = `${absUpChecks.size}/${totalLocations}`; + } + + if (totalLocations > 1) { + return i18n.translate('xpack.uptime.monitorList.statusColumn.locStatusMessage.multiple', { + defaultMessage: 'in {noLoc} Locations', + values: { noLoc: statusMessage }, + }); + } + + return i18n.translate('xpack.uptime.monitorList.statusColumn.locStatusMessage', { + defaultMessage: 'in {noLoc} Location', + values: { noLoc: statusMessage }, + }); +}; + export const MonitorListStatusColumn = ({ status, + checks = [], timestamp: tsString, }: MonitorListStatusColumnProps) => { const timestamp = parseTimestamp(tsString); return ( - - + + {getHealthMessage(status)} - - {timestamp.toLocaleString()} + + + {timestamp.toLocaleString()} + + } + > + + {getRelativeShortTimeStamp(tsString)} - } - > - - {timestamp.fromNow()} - - + + + + + {getLocationStatus(checks, status)} - + ); }; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/translations.ts b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/translations.ts index beacdec1ae2654..5252d90215e954 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/translations.ts +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/translations.ts @@ -56,3 +56,11 @@ export const NO_DATA_MESSAGE = i18n.translate('xpack.uptime.monitorList.noItemMe export const URL = i18n.translate('xpack.uptime.monitorList.table.url.name', { defaultMessage: 'Url', }); + +export const UP = i18n.translate('xpack.uptime.monitorList.statusColumn.upLabel', { + defaultMessage: 'Up', +}); + +export const DOWN = i18n.translate('xpack.uptime.monitorList.statusColumn.downLabel', { + defaultMessage: 'Down', +}); From 440a37a6dbc13427755e027418ee5e3aed482295 Mon Sep 17 00:00:00 2001 From: Kaarina Tungseth Date: Fri, 31 Jan 2020 11:11:07 -0600 Subject: [PATCH 07/33] [DOCS] Share options for Visualize (#56407) * [DOCS] Share options for Visualize * Removed files * Comments from Gail --- docs/user/visualize.asciidoc | 51 ++++++++++++++++++++++++++++--- docs/visualize/inspector.asciidoc | 11 ------- docs/visualize/saving.asciidoc | 19 ------------ 3 files changed, 47 insertions(+), 34 deletions(-) delete mode 100644 docs/visualize/inspector.asciidoc delete mode 100644 docs/visualize/saving.asciidoc diff --git a/docs/user/visualize.asciidoc b/docs/user/visualize.asciidoc index cfd2bac4989c11..5692fe6d1ae01d 100644 --- a/docs/user/visualize.asciidoc +++ b/docs/user/visualize.asciidoc @@ -75,6 +75,53 @@ modifications to the saved search are automatically reflected in the visualization. To disable automatic updates, you can disconnect a visualization from the saved search. +[float] +[[vis-inspector]] +== Inspect visualizations + +Many visualizations allow you to inspect the query and data behind the visualization. + +. In the {kib} toolbar, click *Inspect*. +. To download the data, click *Download CSV*, then choose one of the following options: +* *Formatted CSV* - Downloads the data in table format. +* *Raw CSV* - Downloads the data as provided. +. To view the requests for collecting data, select *Requests* from the *View* +dropdown. + +[float] +[[save-visualize]] +== Save visualizations +To use your visualizations in <>, you must save them. + +. In the {kib} toolbar, click *Save*. +. Enter the visualization *Title* and optional *Description*, then *Save* the visualization. + +To access the saved visualization, go to *Management > {kib} > Saved Objects*. + +[float] +[[save-visualization-read-only-access]] +==== Read only access +When you have insufficient privileges to save visualizations, the following indicator is +displayed and the *Save* button is not visible. + +For more information, refer to <>. + +[role="screenshot"] +image::visualize/images/read-only-badge.png[Example of Visualize's read only access indicator in Kibana's header] + +[float] +[[visualize-share-options]] +== Share visualizations + +When you've finished your visualization, you can share it outside of {kib}. + +From the *Share* menu, you can: + +* Embed the code in a web page. Users must have {kib} access +to view an embedded visualization. +* Share a direct link to a {kib} visualization. +* Generate a PDF report. +* Generate a PNG report. -- include::{kib-repo-dir}/visualize/visualize_rollup_data.asciidoc[] @@ -95,7 +142,3 @@ include::{kib-repo-dir}/visualize/heatmap.asciidoc[] include::{kib-repo-dir}/visualize/for-dashboard.asciidoc[] include::{kib-repo-dir}/visualize/vega.asciidoc[] - -include::{kib-repo-dir}/visualize/saving.asciidoc[] - -include::{kib-repo-dir}/visualize/inspector.asciidoc[] diff --git a/docs/visualize/inspector.asciidoc b/docs/visualize/inspector.asciidoc deleted file mode 100644 index ed98daea211e16..00000000000000 --- a/docs/visualize/inspector.asciidoc +++ /dev/null @@ -1,11 +0,0 @@ -[[vis-inspector]] -== Inspect visualizations - -Many visualizations allow you to inspect the query and data behind the visualization. - -. In the {kib} toolbar, click *Inspect*. -. To download the data, click *Download CSV*, then choose one of the following options: -* *Formatted CSV* - Downloads the data in table format. -* *Raw CSV* - Downloads the data as provided. -. To view the data collection requests, select *Requests* from the *View* -dropdown. diff --git a/docs/visualize/saving.asciidoc b/docs/visualize/saving.asciidoc deleted file mode 100644 index e3330446bfad16..00000000000000 --- a/docs/visualize/saving.asciidoc +++ /dev/null @@ -1,19 +0,0 @@ -[[save-visualize]] -== Save visualizations -To use your visualizations in <>, you must save them. - -. In the {kib} toolbar, click *Save*. -. Enter the visualization *Title* and optional *Description*, then *Save* the visualization. - -To access the saved visualization, go to *Management > {kib} > Saved Objects*. - -[float] -[[save-visualization-read-only-access]] -==== Read only access -When you have insufficient privileges to save visualizations, the following indicator is -displayed and the *Save* button is not visible. - -[role="screenshot"] -image::visualize/images/read-only-badge.png[Example of Visualize's read only access indicator in Kibana's header] - -For more information, see <>. From e24863ed86bc71aeff5eadadcdc916cd5ed99502 Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Fri, 31 Jan 2020 12:30:19 -0500 Subject: [PATCH 08/33] [SIEM][Detection Engine] Modified gap detection util to accept all dateMath formats (#56055) * Partial commit - got tests working and `getGapBetweenRuns` working with `from` and `to` being any string (invalid, ISO string, now, now-x). Co-authored-by: Elastic Machine --- .../detection_engine/signals/utils.test.ts | 136 +++++++++++++----- .../lib/detection_engine/signals/utils.ts | 30 ++-- 2 files changed, 123 insertions(+), 43 deletions(-) diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/utils.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/utils.test.ts index d6a3da5a393f8c..bf25ab8bfd7ea0 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/utils.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/utils.test.ts @@ -5,14 +5,29 @@ */ import moment from 'moment'; +import sinon from 'sinon'; -import { generateId, parseInterval, getDriftTolerance, getGapBetweenRuns } from './utils'; +import { + generateId, + parseInterval, + parseScheduleDates, + getDriftTolerance, + getGapBetweenRuns, +} from './utils'; describe('utils', () => { + const anchor = '2020-01-01T06:06:06.666Z'; + const unix = moment(anchor).valueOf(); let nowDate = moment('2020-01-01T00:00:00.000Z'); + let clock: sinon.SinonFakeTimers; beforeEach(() => { nowDate = moment('2020-01-01T00:00:00.000Z'); + clock = sinon.useFakeTimers(unix); + }); + + afterEach(() => { + clock.restore(); }); describe('generateId', () => { @@ -27,7 +42,7 @@ describe('utils', () => { }); }); - describe('getIntervalMilliseconds', () => { + describe('parseInterval', () => { test('it returns a duration when given one that is valid', () => { const duration = parseInterval('5m'); expect(duration).not.toBeNull(); @@ -40,8 +55,36 @@ describe('utils', () => { }); }); - describe('getDriftToleranceMilliseconds', () => { - test('it returns a drift tolerance in milliseconds of 1 minute when from overlaps to by 1 minute and the interval is 5 minutes', () => { + describe('parseScheduleDates', () => { + test('it returns a moment when given an ISO string', () => { + const result = parseScheduleDates('2020-01-01T00:00:00.000Z'); + expect(result).not.toBeNull(); + expect(result).toEqual(moment('2020-01-01T00:00:00.000Z')); + }); + + test('it returns a moment when given `now`', () => { + const result = parseScheduleDates('now'); + + expect(result).not.toBeNull(); + expect(moment.isMoment(result)).toBeTruthy(); + }); + + test('it returns a moment when given `now-x`', () => { + const result = parseScheduleDates('now-6m'); + + expect(result).not.toBeNull(); + expect(moment.isMoment(result)).toBeTruthy(); + }); + + test('it returns null when given a string that is not an ISO string, `now` or `now-x`', () => { + const result = parseScheduleDates('invalid'); + + expect(result).toBeNull(); + }); + }); + + describe('getDriftTolerance', () => { + test('it returns a drift tolerance in milliseconds of 1 minute when "from" overlaps "to" by 1 minute and the interval is 5 minutes', () => { const drift = getDriftTolerance({ from: 'now-6m', to: 'now', @@ -51,7 +94,7 @@ describe('utils', () => { expect(drift?.asMilliseconds()).toEqual(moment.duration(1, 'minute').asMilliseconds()); }); - test('it returns a drift tolerance of 0 when from equals the interval', () => { + test('it returns a drift tolerance of 0 when "from" equals the interval', () => { const drift = getDriftTolerance({ from: 'now-5m', to: 'now', @@ -60,7 +103,7 @@ describe('utils', () => { expect(drift?.asMilliseconds()).toEqual(0); }); - test('it returns a drift tolerance of 5 minutes when from is 10 minutes but the interval is 5 minutes', () => { + test('it returns a drift tolerance of 5 minutes when "from" is 10 minutes but the interval is 5 minutes', () => { const drift = getDriftTolerance({ from: 'now-10m', to: 'now', @@ -70,7 +113,7 @@ describe('utils', () => { expect(drift?.asMilliseconds()).toEqual(moment.duration(5, 'minutes').asMilliseconds()); }); - test('it returns a drift tolerance of 10 minutes when from is 10 minutes ago and the interval is 0', () => { + test('it returns a drift tolerance of 10 minutes when "from" is 10 minutes ago and the interval is 0', () => { const drift = getDriftTolerance({ from: 'now-10m', to: 'now', @@ -80,36 +123,61 @@ describe('utils', () => { expect(drift?.asMilliseconds()).toEqual(moment.duration(10, 'minutes').asMilliseconds()); }); - test('returns null if the "to" is not "now" since we have limited support for date math', () => { + test('returns a drift tolerance of 1 minute when "from" is invalid and defaults to "now-6m" and interval is 5 minutes', () => { const drift = getDriftTolerance({ - from: 'now-6m', - to: 'invalid', // if not set to "now" this function returns null - interval: moment.duration(1000, 'milliseconds'), + from: 'invalid', + to: 'now', + interval: moment.duration(5, 'minutes'), }); - expect(drift).toBeNull(); + expect(drift).not.toBeNull(); + expect(drift?.asMilliseconds()).toEqual(moment.duration(1, 'minute').asMilliseconds()); }); - test('returns null if the "from" does not start with "now-" since we have limited support for date math', () => { + test('returns a drift tolerance of 1 minute when "from" does not include `now` and defaults to "now-6m" and interval is 5 minutes', () => { const drift = getDriftTolerance({ - from: 'valid', // if not set to "now-x" where x is an interval such as 6m + from: '10m', to: 'now', - interval: moment.duration(1000, 'milliseconds'), + interval: moment.duration(5, 'minutes'), + }); + expect(drift).not.toBeNull(); + expect(drift?.asMilliseconds()).toEqual(moment.duration(1, 'minute').asMilliseconds()); + }); + + test('returns a drift tolerance of 4 minutes when "to" is "now-x", from is a valid input and interval is 5 minute', () => { + const drift = getDriftTolerance({ + from: 'now-10m', + to: 'now-1m', + interval: moment.duration(5, 'minutes'), }); - expect(drift).toBeNull(); + expect(drift).not.toBeNull(); + expect(drift?.asMilliseconds()).toEqual(moment.duration(4, 'minutes').asMilliseconds()); }); - test('returns null if the "from" starts with "now-" but has a string instead of an integer', () => { + test('it returns expected drift tolerance when "from" is an ISO string', () => { const drift = getDriftTolerance({ - from: 'now-dfdf', // if not set to "now-x" where x is an interval such as 6m + from: moment() + .subtract(10, 'minutes') + .toISOString(), to: 'now', - interval: moment.duration(1000, 'milliseconds'), + interval: moment.duration(5, 'minutes'), }); - expect(drift).toBeNull(); + expect(drift).not.toBeNull(); + expect(drift?.asMilliseconds()).toEqual(moment.duration(5, 'minutes').asMilliseconds()); + }); + + test('it returns expected drift tolerance when "to" is an ISO string', () => { + const drift = getDriftTolerance({ + from: 'now-6m', + to: moment().toISOString(), + interval: moment.duration(5, 'minutes'), + }); + expect(drift).not.toBeNull(); + expect(drift?.asMilliseconds()).toEqual(moment.duration(1, 'minute').asMilliseconds()); }); }); describe('getGapBetweenRuns', () => { - test('it returns a gap of 0 when from and interval match each other and the previous started was from the previous interval time', () => { + test('it returns a gap of 0 when "from" and interval match each other and the previous started was from the previous interval time', () => { const gap = getGapBetweenRuns({ previousStartedAt: nowDate.clone().subtract(5, 'minutes'), interval: '5m', @@ -121,7 +189,7 @@ describe('utils', () => { expect(gap?.asMilliseconds()).toEqual(0); }); - test('it returns a negative gap of 1 minute when from overlaps to by 1 minute and the previousStartedAt was 5 minutes ago', () => { + test('it returns a negative gap of 1 minute when "from" overlaps to by 1 minute and the previousStartedAt was 5 minutes ago', () => { const gap = getGapBetweenRuns({ previousStartedAt: nowDate.clone().subtract(5, 'minutes'), interval: '5m', @@ -133,7 +201,7 @@ describe('utils', () => { expect(gap?.asMilliseconds()).toEqual(moment.duration(-1, 'minute').asMilliseconds()); }); - test('it returns a negative gap of 5 minutes when from overlaps to by 1 minute and the previousStartedAt was 5 minutes ago', () => { + test('it returns a negative gap of 5 minutes when "from" overlaps to by 1 minute and the previousStartedAt was 5 minutes ago', () => { const gap = getGapBetweenRuns({ previousStartedAt: nowDate.clone().subtract(5, 'minutes'), interval: '5m', @@ -145,7 +213,7 @@ describe('utils', () => { expect(gap?.asMilliseconds()).toEqual(moment.duration(-5, 'minute').asMilliseconds()); }); - test('it returns a negative gap of 1 minute when from overlaps to by 1 minute and the previousStartedAt was 10 minutes ago and so was the interval', () => { + test('it returns a negative gap of 1 minute when "from" overlaps to by 1 minute and the previousStartedAt was 10 minutes ago and so was the interval', () => { const gap = getGapBetweenRuns({ previousStartedAt: nowDate.clone().subtract(10, 'minutes'), interval: '10m', @@ -233,26 +301,28 @@ describe('utils', () => { expect(gap).toBeNull(); }); - test('it returns null if from is an invalid string such as "invalid"', () => { + test('it returns the expected result when "from" is an invalid string such as "invalid"', () => { const gap = getGapBetweenRuns({ - previousStartedAt: nowDate.clone(), + previousStartedAt: nowDate.clone().subtract(7, 'minutes'), interval: '5m', - from: 'invalid', // if not set to "now-x" where x is an interval such as 6m + from: 'invalid', to: 'now', now: nowDate.clone(), }); - expect(gap).toBeNull(); + expect(gap?.asMilliseconds()).not.toBeNull(); + expect(gap?.asMilliseconds()).toEqual(moment.duration(1, 'minute').asMilliseconds()); }); - test('it returns null if to is an invalid string such as "invalid"', () => { + test('it returns the expected result when "to" is an invalid string such as "invalid"', () => { const gap = getGapBetweenRuns({ - previousStartedAt: nowDate.clone(), + previousStartedAt: nowDate.clone().subtract(7, 'minutes'), interval: '5m', - from: 'now-5m', - to: 'invalid', // if not set to "now" this function returns null + from: 'now-6m', + to: 'invalid', now: nowDate.clone(), }); - expect(gap).toBeNull(); + expect(gap?.asMilliseconds()).not.toBeNull(); + expect(gap?.asMilliseconds()).toEqual(moment.duration(1, 'minute').asMilliseconds()); }); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/utils.ts index 5a4c67ebaaa362..940ea8be2ac36a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/utils.ts @@ -5,6 +5,7 @@ */ import { createHash } from 'crypto'; import moment from 'moment'; +import dateMath from '@elastic/datemath'; import { parseDuration } from '../../../../../alerting/server/lib'; @@ -26,25 +27,34 @@ export const parseInterval = (intervalString: string): moment.Duration | null => } }; +export const parseScheduleDates = (time: string): moment.Moment | null => { + const isValidDateString = !isNaN(Date.parse(time)); + const isValidInput = isValidDateString || time.trim().startsWith('now'); + const formattedDate = isValidDateString + ? moment(time) + : isValidInput + ? dateMath.parse(time) + : null; + + return formattedDate ?? null; +}; + export const getDriftTolerance = ({ from, to, interval, + now = moment(), }: { from: string; to: string; interval: moment.Duration; + now?: moment.Moment; }): moment.Duration | null => { - if (to.trim() !== 'now') { - // we only support 'now' for drift detection - return null; - } - if (!from.trim().startsWith('now-')) { - // we only support from tha starts with now for drift detection - return null; - } - const split = from.split('-'); - const duration = parseInterval(split[1]); + const toDate = parseScheduleDates(to) ?? now; + const fromDate = parseScheduleDates(from) ?? dateMath.parse('now-6m'); + const timeSegment = toDate.diff(fromDate); + const duration = moment.duration(timeSegment); + if (duration !== null) { return duration.subtract(interval); } else { From 74ee0d0ba41c4dc60ad2595f85b5a6f4be1696ec Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Fri, 31 Jan 2020 14:26:12 -0500 Subject: [PATCH 09/33] [Docs] - Deprecate kibana_user role (#54606) * rename kibana_user => kibana_admin * add migration note * [DOCS] Fixes link for use in multiple books * i18n fixes after merge from master * fix i18n * Apply suggestions from code review Co-Authored-By: gchaps <33642766+gchaps@users.noreply.github.com> * Apply suggestions from code review Co-Authored-By: Brandon Kobel Co-authored-by: Lisa Cawley Co-authored-by: Elastic Machine Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> Co-authored-by: Brandon Kobel --- docs/management/watcher-ui/index.asciidoc | 2 +- docs/migration/migrate_8_0.asciidoc | 15 ++++++++++++ docs/uptime-guide/security.asciidoc | 4 ++-- docs/user/monitoring/viewing-metrics.asciidoc | 2 +- .../security/authorization/index.asciidoc | 6 ++--- docs/user/security/reporting.asciidoc | 8 +++---- docs/user/security/securing-kibana.asciidoc | 4 ++-- .../public/views/access_denied/index.html | 4 ++-- .../utils/beat_schema/8.0.0/filebeat.ts | 2 +- .../plugins/transform/common/constants.ts | 4 ++-- ...pace_aware_privilege_section.test.tsx.snap | 8 +++---- .../space_aware_privilege_section.tsx | 8 +++---- .../translations/translations/ja-JP.json | 6 ++--- .../translations/translations/zh-CN.json | 6 ++--- .../apis/console/feature_controls.ts | 23 +++++++++++++++++++ .../monitoring/setup/collection/security.js | 2 +- .../apis/short_urls/feature_controls.ts | 4 ++-- .../dashboard_mode/dashboard_view_mode.js | 2 +- .../apps/security/doc_level_security_roles.js | 4 ++-- .../apps/security/field_level_security.js | 8 +++---- .../apps/security/secure_roles_perm.js | 4 ++-- .../functional/apps/security/user_email.js | 4 ++-- x-pack/test/functional/apps/security/users.js | 6 ++--- .../page_objects/monitoring_page.js | 2 +- .../apis/security/kerberos_login.ts | 4 ++-- .../apis/security/pki_auth.ts | 4 ++-- x-pack/test_utils/kbn_server_config.ts | 4 ++-- 27 files changed, 94 insertions(+), 56 deletions(-) diff --git a/docs/management/watcher-ui/index.asciidoc b/docs/management/watcher-ui/index.asciidoc index 79db96d759aa50..44610a2fd34269 100644 --- a/docs/management/watcher-ui/index.asciidoc +++ b/docs/management/watcher-ui/index.asciidoc @@ -34,7 +34,7 @@ If the {es} {security-features} are enabled, you must have the {ref}/security-privileges.html[`manage_watcher` or `monitor_watcher`] cluster privileges to use Watcher in {kib}. -Alternately, you can have the built-in `kibana_user` role +Alternately, you can have the built-in `kibana_admin` role and either of these watcher roles: * `watcher_admin`. You can perform all Watcher actions, including create and edit watches. diff --git a/docs/migration/migrate_8_0.asciidoc b/docs/migration/migrate_8_0.asciidoc index df4d8a0b65ee7f..146d4e97b6cf4a 100644 --- a/docs/migration/migrate_8_0.asciidoc +++ b/docs/migration/migrate_8_0.asciidoc @@ -80,6 +80,21 @@ specified explicitly. *Impact:* Any workflow that involved manually clearing generated bundles will have to be updated with the new path. +[float] +[[breaking_80_user_role_changes]] +=== User role changes + +[float] +==== `kibana_user` role has been removed and `kibana_admin` has been added. + +*Details:* The `kibana_user` role has been removed and `kibana_admin` has been added to better +reflect its intended use. This role continues to grant all access to every +{kib} feature. If you wish to restrict access to specific features, create +custom roles with {kibana-ref}/kibana-privileges.html[{kib} privileges]. + +*Impact:* Any users currently assigned the `kibana_user` role will need to +instead be assigned the `kibana_admin` role to maintain their current +access level. [float] [[breaking_80_reporting_changes]] diff --git a/docs/uptime-guide/security.asciidoc b/docs/uptime-guide/security.asciidoc index 2a960348b1e02c..6651b33ea0e0e8 100644 --- a/docs/uptime-guide/security.asciidoc +++ b/docs/uptime-guide/security.asciidoc @@ -42,7 +42,7 @@ PUT /_security/role/uptime === Assign the role to a user Next, you'll need to create a user with both the `uptime` role, and another role with sufficient {kibana-ref}/kibana-privileges.html[Kibana privileges], -such as the `kibana_user` role. +such as the `kibana_admin` role. You can do this with the following request: ["source","sh",subs="attributes,callouts"] @@ -50,7 +50,7 @@ You can do this with the following request: PUT /_security/user/jacknich { "password" : "j@rV1s", - "roles" : [ "uptime", "kibana_user" ], + "roles" : [ "uptime", "kibana_admin" ], "full_name" : "Jack Nicholson", "email" : "jacknich@example.com", "metadata" : { diff --git a/docs/user/monitoring/viewing-metrics.asciidoc b/docs/user/monitoring/viewing-metrics.asciidoc index 61bcb9a49c9012..11516e32400fb9 100644 --- a/docs/user/monitoring/viewing-metrics.asciidoc +++ b/docs/user/monitoring/viewing-metrics.asciidoc @@ -63,7 +63,7 @@ remote monitoring cluster, you must use credentials that are valid on both the -- -.. Create users that have the `monitoring_user` and `kibana_user` +.. Create users that have the `monitoring_user` and `kibana_admin` {ref}/built-in-roles.html[built-in roles]. . Open {kib} in your web browser. diff --git a/docs/user/security/authorization/index.asciidoc b/docs/user/security/authorization/index.asciidoc index 2636b3dfc1bd38..853c735418cea8 100644 --- a/docs/user/security/authorization/index.asciidoc +++ b/docs/user/security/authorization/index.asciidoc @@ -2,11 +2,11 @@ [[xpack-security-authorization]] === Granting access to {kib} -The Elastic Stack comes with the `kibana_user` {ref}/built-in-roles.html[built-in role], which you can use to grant access to all Kibana features in all spaces. To grant users access to a subset of spaces or features, you can create a custom role that grants the desired Kibana privileges. +The Elastic Stack comes with the `kibana_admin` {ref}/built-in-roles.html[built-in role], which you can use to grant access to all Kibana features in all spaces. To grant users access to a subset of spaces or features, you can create a custom role that grants the desired Kibana privileges. -When you assign a user multiple roles, the user receives a union of the roles’ privileges. Therefore, assigning the `kibana_user` role in addition to a custom role that grants Kibana privileges is ineffective because `kibana_user` has access to all the features in all spaces. +When you assign a user multiple roles, the user receives a union of the roles’ privileges. Therefore, assigning the `kibana_admin` role in addition to a custom role that grants Kibana privileges is ineffective because `kibana_admin` has access to all the features in all spaces. -NOTE: When running multiple tenants of Kibana by changing the `kibana.index` in your `kibana.yml`, you cannot use `kibana_user` to grant access. You must create custom roles that authorize the user for that specific tenant. Although multi-tenant installations are supported, the recommended approach to securing access to Kibana segments is to grant users access to specific spaces. +NOTE: When running multiple tenants of Kibana by changing the `kibana.index` in your `kibana.yml`, you cannot use `kibana_admin` to grant access. You must create custom roles that authorize the user for that specific tenant. Although multi-tenant installations are supported, the recommended approach to securing access to Kibana segments is to grant users access to specific spaces. [role="xpack"] === {kib} role management diff --git a/docs/user/security/reporting.asciidoc b/docs/user/security/reporting.asciidoc index 5f5d85fe8d3beb..825580bdc772ed 100644 --- a/docs/user/security/reporting.asciidoc +++ b/docs/user/security/reporting.asciidoc @@ -85,14 +85,14 @@ elasticsearch.username: 'custom_kibana_system' [[reporting-roles-user-api]] ==== With the user API This example uses the {ref}/security-api-put-user.html[user API] to create a user who has the -`reporting_user` role and the `kibana_user` role: +`reporting_user` role and the `kibana_admin` role: [source, sh] --------------------------------------------------------------- POST /_security/user/reporter { "password" : "x-pack-test-password", - "roles" : ["kibana_user", "reporting_user"], + "roles" : ["kibana_admin", "reporting_user"], "full_name" : "Reporting User" } --------------------------------------------------------------- @@ -106,11 +106,11 @@ roles on a per user basis, or assign roles to groups of users. By default, role mappings are configured in {ref}/mapping-roles.html[`config/shield/role_mapping.yml`]. For example, the following snippet assigns the user named Bill Murray the -`kibana_user` and `reporting_user` roles: +`kibana_admin` and `reporting_user` roles: [source,yaml] -------------------------------------------------------------------------------- -kibana_user: +kibana_admin: - "cn=Bill Murray,dc=example,dc=com" reporting_user: - "cn=Bill Murray,dc=example,dc=com" diff --git a/docs/user/security/securing-kibana.asciidoc b/docs/user/security/securing-kibana.asciidoc index 2d07b57bfabe1e..b6b5248777f6ba 100644 --- a/docs/user/security/securing-kibana.asciidoc +++ b/docs/user/security/securing-kibana.asciidoc @@ -104,7 +104,7 @@ You can manage privileges on the *Management / Security / Roles* page in {kib}. If you're using the native realm with Basic Authentication, you can assign roles using the *Management / Security / Users* page in {kib} or the {ref}/security-api.html#security-user-apis[user management APIs]. For example, -the following creates a user named `jacknich` and assigns it the `kibana_user` +the following creates a user named `jacknich` and assigns it the `kibana_admin` role: [source,js] @@ -112,7 +112,7 @@ role: POST /_security/user/jacknich { "password" : "t0pS3cr3t", - "roles" : [ "kibana_user" ] + "roles" : [ "kibana_admin" ] } -------------------------------------------------------------------------------- // CONSOLE diff --git a/x-pack/legacy/plugins/monitoring/public/views/access_denied/index.html b/x-pack/legacy/plugins/monitoring/public/views/access_denied/index.html index 8c67451b86f36e..63cd4440ecf8a2 100644 --- a/x-pack/legacy/plugins/monitoring/public/views/access_denied/index.html +++ b/x-pack/legacy/plugins/monitoring/public/views/access_denied/index.html @@ -15,9 +15,9 @@ class="kuiInfoPanelBody__message" i18n-id="xpack.monitoring.accessDenied.notAuthorizedDescription" i18n-default-message="You are not authorized to access Monitoring. To use Monitoring, you - need the privileges granted by both the `{kibanaUser}` and + need the privileges granted by both the `{kibanaAdmin}` and `{monitoringUser}` roles." - i18n-values="{ kibanaUser: 'kibana_user', monitoringUser: 'monitoring_user' }" + i18n-values="{ kibanaAdmin: 'kibana_admin', monitoringUser: 'monitoring_user' }" >
with user profile disabling "manageSpaces"

+ "kibanaAdmin": , diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.tsx index 21cadfafe1790b..b2b92356e51267 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.tsx @@ -95,13 +95,13 @@ class SpaceAwarePrivilegeSectionUI extends Component { ), diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 817aa03db31bd0..47e11817ffa5d4 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -8908,7 +8908,7 @@ "xpack.ml.validateJob.validateJobButtonLabel": "ジョブを検証", "xpack.monitoring.accessDenied.backToKibanaButtonLabel": "Kibana に戻る", "xpack.monitoring.accessDenied.clusterNotConfiguredDescription": "専用の監視クラスターへのアクセスを試みている場合、監視クラスターで構成されていないユーザーとしてログインしていることが原因である可能性があります。", - "xpack.monitoring.accessDenied.notAuthorizedDescription": "監視アクセスが許可されていません。監視を利用するには、「{kibanaUser}」と「{monitoringUser}」の両方のロールからの権限が必要です。", + "xpack.monitoring.accessDenied.notAuthorizedDescription": "監視アクセスが許可されていません。監視を利用するには、「{kibanaAdmin}」と「{monitoringUser}」の両方のロールからの権限が必要です。", "xpack.monitoring.accessDeniedTitle": "アクセス拒否", "xpack.monitoring.ajaxErrorHandler.httpErrorMessage": "HTTP {errStatus}", "xpack.monitoring.ajaxErrorHandler.requestErrorNotificationTitle": "監視リクエストエラー", @@ -10644,11 +10644,11 @@ "xpack.security.management.editRole.spaceAwarePrivilegeDisplay.spaceBasePrivilegeSource": "スペースベース権限", "xpack.security.management.editRole.spaceAwarePrivilegeDisplay.spaceFeaturePrivilegeSource": "スペース機能権限", "xpack.security.management.editRole.spaceAwarePrivilegeDisplay.unknownPrivilegeSource": "**不明**", - "xpack.security.management.editRole.spaceAwarePrivilegeForm.ensureAccountHasAllPrivilegesGrantedDescription": "{kibanaUser} ロールによりアカウントにすべての権限が提供されていることを確認し、再試行してください。", + "xpack.security.management.editRole.spaceAwarePrivilegeForm.ensureAccountHasAllPrivilegesGrantedDescription": "{kibanaAdmin} ロールによりアカウントにすべての権限が提供されていることを確認し、再試行してください。", "xpack.security.management.editRole.spaceAwarePrivilegeForm.globalSpacesName": "* グローバル (すべてのスペース)", "xpack.security.management.editRole.spaceAwarePrivilegeForm.howToViewAllAvailableSpacesDescription": "利用可能なすべてのスペースを表示する権限がありません。", "xpack.security.management.editRole.spaceAwarePrivilegeForm.insufficientPrivilegesDescription": "権限が不十分です", - "xpack.security.management.editRole.spaceAwarePrivilegeForm.kibanaUserTitle": "kibana_user", + "xpack.security.management.editRole.spaceAwarePrivilegeForm.kibanaAdminTitle": "kibana_admin", "xpack.security.management.editRole.spacePrivilegeForm.allPrivilegeDetails": "選択されたスペースの全機能への完全アクセスを許可します。", "xpack.security.management.editRole.spacePrivilegeForm.allPrivilegeDisplay": "すべて", "xpack.security.management.editRole.spacePrivilegeForm.allPrivilegeDropdownDisplay": "すべて", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index d8012bbb526c99..86d9a69dc0900b 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -8907,7 +8907,7 @@ "xpack.ml.validateJob.validateJobButtonLabel": "验证作业", "xpack.monitoring.accessDenied.backToKibanaButtonLabel": "返回 Kibana", "xpack.monitoring.accessDenied.clusterNotConfiguredDescription": "如果您尝试访问专用监测集群,则这可能是因为该监测集群上未配置您登录时所用的用户帐户。", - "xpack.monitoring.accessDenied.notAuthorizedDescription": "您无权访问 Monitoring。要使用 Monitoring,您同时需要 `{kibanaUser}` 和 `{monitoringUser}` 角色授予的权限。", + "xpack.monitoring.accessDenied.notAuthorizedDescription": "您无权访问 Monitoring。要使用 Monitoring,您同时需要 `{kibanaAdmin}` 和 `{monitoringUser}` 角色授予的权限。", "xpack.monitoring.accessDeniedTitle": "访问被拒绝", "xpack.monitoring.ajaxErrorHandler.httpErrorMessage": "HTTP {errStatus}", "xpack.monitoring.ajaxErrorHandler.requestErrorNotificationTitle": "Monitoring 请求错误", @@ -10643,11 +10643,11 @@ "xpack.security.management.editRole.spaceAwarePrivilegeDisplay.spaceBasePrivilegeSource": "工作区基本权限", "xpack.security.management.editRole.spaceAwarePrivilegeDisplay.spaceFeaturePrivilegeSource": "全局功能权限", "xpack.security.management.editRole.spaceAwarePrivilegeDisplay.unknownPrivilegeSource": "**未知**", - "xpack.security.management.editRole.spaceAwarePrivilegeForm.ensureAccountHasAllPrivilegesGrantedDescription": "请确保您的帐户具有 {kibanaUser} 角色授予的所有权限,然后重试。", + "xpack.security.management.editRole.spaceAwarePrivilegeForm.ensureAccountHasAllPrivilegesGrantedDescription": "请确保您的帐户具有 {kibanaAdmin} 角色授予的所有权限,然后重试。", "xpack.security.management.editRole.spaceAwarePrivilegeForm.globalSpacesName": "* 全局(所有工作区)", "xpack.security.management.editRole.spaceAwarePrivilegeForm.howToViewAllAvailableSpacesDescription": "您无权查看所有可用工作区。", "xpack.security.management.editRole.spaceAwarePrivilegeForm.insufficientPrivilegesDescription": "权限不足", - "xpack.security.management.editRole.spaceAwarePrivilegeForm.kibanaUserTitle": "kibana_user", + "xpack.security.management.editRole.spaceAwarePrivilegeForm.kibanaAdminTitle": "kibana_admin", "xpack.security.management.editRole.spacePrivilegeForm.allPrivilegeDetails": "授予对选定工作区所有功能的完全访问权限。", "xpack.security.management.editRole.spacePrivilegeForm.allPrivilegeDisplay": "全部", "xpack.security.management.editRole.spacePrivilegeForm.allPrivilegeDropdownDisplay": "全部", diff --git a/x-pack/test/api_integration/apis/console/feature_controls.ts b/x-pack/test/api_integration/apis/console/feature_controls.ts index 3f9a0867794378..ce926f0d032c8b 100644 --- a/x-pack/test/api_integration/apis/console/feature_controls.ts +++ b/x-pack/test/api_integration/apis/console/feature_controls.ts @@ -43,6 +43,29 @@ export default function securityTests({ getService }: FtrProviderContext) { } }); + it('can be accessed by kibana_admin role', async () => { + const username = 'kibana_admin'; + const roleName = 'kibana_admin'; + try { + const password = `${username}-password`; + + await security.user.create(username, { + password, + roles: [roleName], + full_name: 'a kibana admin', + }); + + await supertest + .post(`/api/console/proxy?method=GET&path=${encodeURIComponent('/_cat')}`) + .auth(username, password) + .set('kbn-xsrf', 'xxx') + .send() + .expect(200); + } finally { + await security.user.delete(username); + } + }); + it('can be accessed by global all role', async () => { const username = 'global_all'; const roleName = 'global_all'; diff --git a/x-pack/test/api_integration/apis/monitoring/setup/collection/security.js b/x-pack/test/api_integration/apis/monitoring/setup/collection/security.js index 7e6a2dbe319659..4da08d7cb97265 100644 --- a/x-pack/test/api_integration/apis/monitoring/setup/collection/security.js +++ b/x-pack/test/api_integration/apis/monitoring/setup/collection/security.js @@ -44,7 +44,7 @@ export default function({ getService }) { await security.user.create(username, { password: password, full_name: 'Limited User', - roles: ['kibana_user', 'monitoring_user'], + roles: ['kibana_admin', 'monitoring_user'], }); const { body } = await supertestWithoutAuth diff --git a/x-pack/test/api_integration/apis/short_urls/feature_controls.ts b/x-pack/test/api_integration/apis/short_urls/feature_controls.ts index db5e11ef367ad7..35a6f2c2b382a6 100644 --- a/x-pack/test/api_integration/apis/short_urls/feature_controls.ts +++ b/x-pack/test/api_integration/apis/short_urls/feature_controls.ts @@ -12,8 +12,8 @@ export default function featureControlsTests({ getService }: FtrProviderContext) const security = getService('security'); describe('feature controls', () => { - const kibanaUsername = 'kibana_user'; - const kibanaUserRoleName = 'kibana_user'; + const kibanaUsername = 'kibana_admin'; + const kibanaUserRoleName = 'kibana_admin'; const kibanaUserPassword = `${kibanaUsername}-password`; diff --git a/x-pack/test/functional/apps/dashboard_mode/dashboard_view_mode.js b/x-pack/test/functional/apps/dashboard_mode/dashboard_view_mode.js index 1189fe909ca320..b521c47585d58b 100644 --- a/x-pack/test/functional/apps/dashboard_mode/dashboard_view_mode.js +++ b/x-pack/test/functional/apps/dashboard_mode/dashboard_view_mode.js @@ -92,7 +92,7 @@ export default function({ getService, getPageObjects }) { await testSubjects.setValue('userFormFullNameInput', 'mixeduser'); await testSubjects.setValue('userFormEmailInput', 'example@example.com'); await PageObjects.security.assignRoleToUser('kibana_dashboard_only_user'); - await PageObjects.security.assignRoleToUser('kibana_user'); + await PageObjects.security.assignRoleToUser('kibana_admin'); await PageObjects.security.assignRoleToUser('logstash-data'); await PageObjects.security.clickSaveEditUser(); diff --git a/x-pack/test/functional/apps/security/doc_level_security_roles.js b/x-pack/test/functional/apps/security/doc_level_security_roles.js index 5761369f9e4685..480fa6599e0365 100644 --- a/x-pack/test/functional/apps/security/doc_level_security_roles.js +++ b/x-pack/test/functional/apps/security/doc_level_security_roles.js @@ -58,11 +58,11 @@ export default function({ getService, getPageObjects }) { fullname: 'dls EAST', email: 'dlstest@elastic.com', save: true, - roles: ['kibana_user', 'myroleEast'], + roles: ['kibana_admin', 'myroleEast'], }); const users = indexBy(await PageObjects.security.getElasticsearchUsers(), 'username'); log.debug('actualUsers = %j', users); - expect(users.userEast.roles).to.eql(['kibana_user', 'myroleEast']); + expect(users.userEast.roles).to.eql(['kibana_admin', 'myroleEast']); expect(users.userEast.reserved).to.be(false); }); diff --git a/x-pack/test/functional/apps/security/field_level_security.js b/x-pack/test/functional/apps/security/field_level_security.js index 16e9d755bf261b..93a6f9cd9e0c3d 100644 --- a/x-pack/test/functional/apps/security/field_level_security.js +++ b/x-pack/test/functional/apps/security/field_level_security.js @@ -79,11 +79,11 @@ export default function({ getService, getPageObjects }) { fullname: 'customer one', email: 'flstest@elastic.com', save: true, - roles: ['kibana_user', 'a_viewssnrole'], + roles: ['kibana_admin', 'a_viewssnrole'], }); const users = indexBy(await PageObjects.security.getElasticsearchUsers(), 'username'); log.debug('actualUsers = %j', users); - expect(users.customer1.roles).to.eql(['kibana_user', 'a_viewssnrole']); + expect(users.customer1.roles).to.eql(['kibana_admin', 'a_viewssnrole']); }); it('should add new user customer2 ', async function() { @@ -95,11 +95,11 @@ export default function({ getService, getPageObjects }) { fullname: 'customer two', email: 'flstest@elastic.com', save: true, - roles: ['kibana_user', 'a_view_no_ssn_role'], + roles: ['kibana_admin', 'a_view_no_ssn_role'], }); const users = indexBy(await PageObjects.security.getElasticsearchUsers(), 'username'); log.debug('actualUsers = %j', users); - expect(users.customer2.roles).to.eql(['kibana_user', 'a_view_no_ssn_role']); + expect(users.customer2.roles).to.eql(['kibana_admin', 'a_view_no_ssn_role']); }); it('user customer1 should see ssn', async function() { diff --git a/x-pack/test/functional/apps/security/secure_roles_perm.js b/x-pack/test/functional/apps/security/secure_roles_perm.js index ece289b4a666ee..4e155872d1041a 100644 --- a/x-pack/test/functional/apps/security/secure_roles_perm.js +++ b/x-pack/test/functional/apps/security/secure_roles_perm.js @@ -61,13 +61,13 @@ export default function({ getService, getPageObjects }) { fullname: 'RashmiFirst RashmiLast', email: 'rashmi@myEmail.com', save: true, - roles: ['logstash_reader', 'kibana_user'], + roles: ['logstash_reader', 'kibana_admin'], }); log.debug('After Add user: , userObj.userName'); const users = indexBy(await PageObjects.security.getElasticsearchUsers(), 'username'); log.debug('actualUsers = %j', users); log.debug('roles: ', users.Rashmi.roles); - expect(users.Rashmi.roles).to.eql(['logstash_reader', 'kibana_user']); + expect(users.Rashmi.roles).to.eql(['logstash_reader', 'kibana_admin']); expect(users.Rashmi.fullname).to.eql('RashmiFirst RashmiLast'); expect(users.Rashmi.reserved).to.be(false); await PageObjects.security.forceLogout(); diff --git a/x-pack/test/functional/apps/security/user_email.js b/x-pack/test/functional/apps/security/user_email.js index 492eddcfb9f746..a007c40a06b626 100644 --- a/x-pack/test/functional/apps/security/user_email.js +++ b/x-pack/test/functional/apps/security/user_email.js @@ -27,11 +27,11 @@ export default function({ getService, getPageObjects }) { fullname: 'newuserFirst newuserLast', email: 'newuser@myEmail.com', save: true, - roles: ['kibana_user', 'superuser'], + roles: ['kibana_admin', 'superuser'], }); const users = indexBy(await PageObjects.security.getElasticsearchUsers(), 'username'); log.debug('actualUsers = %j', users); - expect(users.newuser.roles).to.eql(['kibana_user', 'superuser']); + expect(users.newuser.roles).to.eql(['kibana_admin', 'superuser']); expect(users.newuser.fullname).to.eql('newuserFirst newuserLast'); expect(users.newuser.email).to.eql('newuser@myEmail.com'); expect(users.newuser.reserved).to.be(false); diff --git a/x-pack/test/functional/apps/security/users.js b/x-pack/test/functional/apps/security/users.js index 3eed74881e9571..9dc42553f0fdfe 100644 --- a/x-pack/test/functional/apps/security/users.js +++ b/x-pack/test/functional/apps/security/users.js @@ -42,11 +42,11 @@ export default function({ getService, getPageObjects }) { fullname: 'LeeFirst LeeLast', email: 'lee@myEmail.com', save: true, - roles: ['kibana_user'], + roles: ['kibana_admin'], }); const users = indexBy(await PageObjects.security.getElasticsearchUsers(), 'username'); log.debug('actualUsers = %j', users); - expect(users.Lee.roles).to.eql(['kibana_user']); + expect(users.Lee.roles).to.eql(['kibana_admin']); expect(users.Lee.fullname).to.eql('LeeFirst LeeLast'); expect(users.Lee.email).to.eql('lee@myEmail.com'); expect(users.Lee.reserved).to.be(false); @@ -85,7 +85,7 @@ export default function({ getService, getPageObjects }) { expect(roles.apm_user.reserved).to.be(true); expect(roles.beats_admin.reserved).to.be(true); expect(roles.beats_system.reserved).to.be(true); - expect(roles.kibana_user.reserved).to.be(true); + expect(roles.kibana_admin.reserved).to.be(true); expect(roles.kibana_system.reserved).to.be(true); expect(roles.logstash_system.reserved).to.be(true); expect(roles.monitoring_user.reserved).to.be(true); diff --git a/x-pack/test/functional/page_objects/monitoring_page.js b/x-pack/test/functional/page_objects/monitoring_page.js index 6920010d67187f..8de5b5e69d34d7 100644 --- a/x-pack/test/functional/page_objects/monitoring_page.js +++ b/x-pack/test/functional/page_objects/monitoring_page.js @@ -14,7 +14,7 @@ export function MonitoringPageProvider({ getPageObjects, getService }) { // always create this because our tear down tries to delete it await security.user.create('basic_monitoring_user', { password: 'monitoring_user_password', - roles: ['monitoring_user', 'kibana_user'], + roles: ['monitoring_user', 'kibana_admin'], full_name: 'basic monitoring', }); diff --git a/x-pack/test/kerberos_api_integration/apis/security/kerberos_login.ts b/x-pack/test/kerberos_api_integration/apis/security/kerberos_login.ts index 0346da334d2f2d..203f90c55aa82b 100644 --- a/x-pack/test/kerberos_api_integration/apis/security/kerberos_login.ts +++ b/x-pack/test/kerberos_api_integration/apis/security/kerberos_login.ts @@ -38,7 +38,7 @@ export default function({ getService }: FtrProviderContext) { await getService('esSupertest') .post('/_security/role_mapping/krb5') .send({ - roles: ['kibana_user'], + roles: ['kibana_admin'], enabled: true, rules: { field: { 'realm.name': 'kerb1' } }, }) @@ -119,7 +119,7 @@ export default function({ getService }: FtrProviderContext) { .set('Cookie', sessionCookie.cookieString()) .expect(200, { username: 'tester@TEST.ELASTIC.CO', - roles: ['kibana_user'], + roles: ['kibana_admin'], full_name: null, email: null, metadata: { diff --git a/x-pack/test/pki_api_integration/apis/security/pki_auth.ts b/x-pack/test/pki_api_integration/apis/security/pki_auth.ts index 4eee900e68bec9..186ed824b3b6c7 100644 --- a/x-pack/test/pki_api_integration/apis/security/pki_auth.ts +++ b/x-pack/test/pki_api_integration/apis/security/pki_auth.ts @@ -48,7 +48,7 @@ export default function({ getService }: FtrProviderContext) { .post('/_security/role_mapping/first_client_pki') .ca(CA_CERT) .send({ - roles: ['kibana_user'], + roles: ['kibana_admin'], enabled: true, rules: { field: { dn: 'CN=first_client' } }, }) @@ -107,7 +107,7 @@ export default function({ getService }: FtrProviderContext) { expect(response.body).to.eql({ username: 'first_client', - roles: ['kibana_user'], + roles: ['kibana_admin'], full_name: null, email: null, enabled: true, diff --git a/x-pack/test_utils/kbn_server_config.ts b/x-pack/test_utils/kbn_server_config.ts index 75f5ac736b7c07..3cac6ed5df0147 100644 --- a/x-pack/test_utils/kbn_server_config.ts +++ b/x-pack/test_utils/kbn_server_config.ts @@ -26,9 +26,9 @@ export const TestKbnServerConfig = { }, users: [ { - username: 'kibana_user', + username: 'kibana_admin', password: 'x-pack-test-password', - roles: ['kibana_user'], + roles: ['kibana_admin'], }, ], }; From c2a6e7571b4803558b004f9d64c409a0a58ccab3 Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Fri, 31 Jan 2020 12:50:17 -0700 Subject: [PATCH 10/33] [Reporting] Use ES plugin from NP (#56209) * [Reporting] Use ES plugin from NP * fix elasticsearchErrors reference * fix mocha test * convert to jest * fix the code and tests * cosmetics * fix mocha tests * fix imports * fix mocha tests * fix jest * simplify Co-authored-by: Elastic Machine --- .../csv/server/execute_job.test.js | 272 +++++++++--------- .../export_types/csv/server/execute_job.ts | 15 +- .../server/create_job/create_job.ts | 15 +- .../server/execute_job.ts | 8 +- .../server/lib/generate_csv.ts | 3 + .../server/lib/generate_csv_search.ts | 11 +- .../png/server/execute_job/index.test.js | 27 +- .../png/server/execute_job/index.ts | 2 + .../server/execute_job/index.test.js | 26 +- .../printable_pdf/server/execute_job/index.ts | 2 + x-pack/legacy/plugins/reporting/index.ts | 29 +- .../reporting/server/lib/create_queue.ts | 6 +- .../server/lib/create_worker.test.ts | 31 +- .../reporting/server/lib/create_worker.ts | 6 +- .../reporting/server/lib/enqueue_job.ts | 4 +- .../fixtures/legacy_elasticsearch.js | 34 ++- .../esqueue/__tests__/helpers/create_index.js | 6 +- .../server/lib/esqueue/__tests__/index.js | 2 +- .../server/lib/esqueue/__tests__/job.js | 16 +- .../server/lib/esqueue/__tests__/worker.js | 66 ++--- .../lib/esqueue/helpers/create_index.js | 4 +- .../reporting/server/lib/esqueue/index.js | 2 +- .../reporting/server/lib/esqueue/job.js | 6 +- .../reporting/server/lib/esqueue/worker.js | 8 +- .../reporting/server/lib/jobs_query.ts | 14 +- .../reporting/server/lib/validate/index.ts | 6 +- ...js => validate_max_content_length.test.js} | 57 ++-- .../validate/validate_max_content_length.ts | 12 +- .../legacy/plugins/reporting/server/plugin.ts | 32 +-- .../generate_from_savedobject_immediate.ts | 5 +- .../reporting/server/routes/generation.ts | 17 +- .../reporting/server/routes/jobs.test.js | 50 ++-- .../plugins/reporting/server/routes/jobs.ts | 5 +- .../server/routes/lib/job_response_handler.ts | 8 +- x-pack/legacy/plugins/reporting/types.d.ts | 13 +- 35 files changed, 441 insertions(+), 379 deletions(-) rename x-pack/legacy/plugins/reporting/server/lib/validate/{__tests__/validate_max_content_length.js => validate_max_content_length.test.js} (65%) diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.test.js b/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.test.js index 1abc923d340e60..06ba36f600b9ba 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.test.js +++ b/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.test.js @@ -37,9 +37,15 @@ describe('CSV Execute Job', function() { let cancellationToken; let mockServer; let clusterStub; - let callWithRequestStub; + let callAsCurrentUserStub; let uiSettingsGetStub; + const mockElasticsearch = { + dataClient: { + asScoped: () => clusterStub, + }, + }; + beforeAll(async function() { const crypto = nodeCrypto({ encryptionKey }); encryptedHeaders = await crypto.encrypt(headers); @@ -55,11 +61,11 @@ describe('CSV Execute Job', function() { _scroll_id: 'defaultScrollId', }; clusterStub = { - callWithRequest: function() {}, + callAsCurrentUser: function() {}, }; - callWithRequestStub = sinon - .stub(clusterStub, 'callWithRequest') + callAsCurrentUserStub = sinon + .stub(clusterStub, 'callAsCurrentUser') .resolves(defaultElasticsearchResponse); const configGetStub = sinon.stub(); @@ -68,7 +74,6 @@ describe('CSV Execute Job', function() { uiSettingsGetStub.withArgs('csv:quoteValues').returns(true); mockServer = { - expose: function() {}, fieldFormatServiceFactory: function() { const uiConfigMock = {}; uiConfigMock['format:defaultTypeMap'] = { @@ -81,13 +86,6 @@ describe('CSV Execute Job', function() { return fieldFormatsRegistry; }, - plugins: { - elasticsearch: { - getCluster: function() { - return clusterStub; - }, - }, - }, config: function() { return { get: configGetStub, @@ -117,7 +115,7 @@ describe('CSV Execute Job', function() { describe('calls getScopedSavedObjectsClient with request', function() { it('containing decrypted headers', async function() { - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); await executeJob( 'job456', { headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null } }, @@ -135,7 +133,7 @@ describe('CSV Execute Job', function() { .config() .get.withArgs('server.basePath') .returns(serverBasePath); - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); await executeJob( 'job456', { headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null } }, @@ -153,7 +151,7 @@ describe('CSV Execute Job', function() { .config() .get.withArgs('server.basePath') .returns(serverBasePath); - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); const jobBasePath = 'foo-job/basePath/'; await executeJob( 'job789', @@ -176,7 +174,7 @@ describe('CSV Execute Job', function() { it('passed scoped SavedObjectsClient to uiSettingsServiceFactory', async function() { const returnValue = Symbol(); mockServer.savedObjects.getScopedSavedObjectsClient.returns(returnValue); - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); await executeJob( 'job456', { headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null } }, @@ -190,15 +188,15 @@ describe('CSV Execute Job', function() { }); describe('basic Elasticsearch call behavior', function() { - it('should decrypt encrypted headers and pass to callWithRequest', async function() { - const executeJob = executeJobFactory(mockServer, mockLogger); + it('should decrypt encrypted headers and pass to callAsCurrentUser', async function() { + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); await executeJob( 'job456', { headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null } }, cancellationToken ); - expect(callWithRequestStub.called).toBe(true); - expect(callWithRequestStub.firstCall.args[0].headers).toEqual(headers); + expect(callAsCurrentUserStub.called).toBe(true); + expect(callAsCurrentUserStub.firstCall.args[0]).toEqual('search'); }); it('should pass the index and body to execute the initial search', async function() { @@ -207,7 +205,7 @@ describe('CSV Execute Job', function() { testBody: true, }; - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); const job = { headers: encryptedHeaders, fields: [], @@ -219,115 +217,115 @@ describe('CSV Execute Job', function() { await executeJob('job777', job, cancellationToken); - const searchCall = callWithRequestStub.firstCall; - expect(searchCall.args[1]).toBe('search'); - expect(searchCall.args[2].index).toBe(index); - expect(searchCall.args[2].body).toBe(body); + const searchCall = callAsCurrentUserStub.firstCall; + expect(searchCall.args[0]).toBe('search'); + expect(searchCall.args[1].index).toBe(index); + expect(searchCall.args[1].body).toBe(body); }); it('should pass the scrollId from the initial search to the subsequent scroll', async function() { const scrollId = getRandomScrollId(); - callWithRequestStub.onFirstCall().resolves({ + callAsCurrentUserStub.onFirstCall().resolves({ hits: { hits: [{}], }, _scroll_id: scrollId, }); - callWithRequestStub.onSecondCall().resolves(defaultElasticsearchResponse); - const executeJob = executeJobFactory(mockServer, mockLogger); + callAsCurrentUserStub.onSecondCall().resolves(defaultElasticsearchResponse); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); await executeJob( 'job456', { headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null } }, cancellationToken ); - const scrollCall = callWithRequestStub.secondCall; + const scrollCall = callAsCurrentUserStub.secondCall; - expect(scrollCall.args[1]).toBe('scroll'); - expect(scrollCall.args[2].scrollId).toBe(scrollId); + expect(scrollCall.args[0]).toBe('scroll'); + expect(scrollCall.args[1].scrollId).toBe(scrollId); }); it('should not execute scroll if there are no hits from the search', async function() { - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); await executeJob( 'job456', { headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null } }, cancellationToken ); - expect(callWithRequestStub.callCount).toBe(2); + expect(callAsCurrentUserStub.callCount).toBe(2); - const searchCall = callWithRequestStub.firstCall; - expect(searchCall.args[1]).toBe('search'); + const searchCall = callAsCurrentUserStub.firstCall; + expect(searchCall.args[0]).toBe('search'); - const clearScrollCall = callWithRequestStub.secondCall; - expect(clearScrollCall.args[1]).toBe('clearScroll'); + const clearScrollCall = callAsCurrentUserStub.secondCall; + expect(clearScrollCall.args[0]).toBe('clearScroll'); }); it('should stop executing scroll if there are no hits', async function() { - callWithRequestStub.onFirstCall().resolves({ + callAsCurrentUserStub.onFirstCall().resolves({ hits: { hits: [{}], }, _scroll_id: 'scrollId', }); - callWithRequestStub.onSecondCall().resolves({ + callAsCurrentUserStub.onSecondCall().resolves({ hits: { hits: [], }, _scroll_id: 'scrollId', }); - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); await executeJob( 'job456', { headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null } }, cancellationToken ); - expect(callWithRequestStub.callCount).toBe(3); + expect(callAsCurrentUserStub.callCount).toBe(3); - const searchCall = callWithRequestStub.firstCall; - expect(searchCall.args[1]).toBe('search'); + const searchCall = callAsCurrentUserStub.firstCall; + expect(searchCall.args[0]).toBe('search'); - const scrollCall = callWithRequestStub.secondCall; - expect(scrollCall.args[1]).toBe('scroll'); + const scrollCall = callAsCurrentUserStub.secondCall; + expect(scrollCall.args[0]).toBe('scroll'); - const clearScroll = callWithRequestStub.thirdCall; - expect(clearScroll.args[1]).toBe('clearScroll'); + const clearScroll = callAsCurrentUserStub.thirdCall; + expect(clearScroll.args[0]).toBe('clearScroll'); }); it('should call clearScroll with scrollId when there are no more hits', async function() { const lastScrollId = getRandomScrollId(); - callWithRequestStub.onFirstCall().resolves({ + callAsCurrentUserStub.onFirstCall().resolves({ hits: { hits: [{}], }, _scroll_id: 'scrollId', }); - callWithRequestStub.onSecondCall().resolves({ + callAsCurrentUserStub.onSecondCall().resolves({ hits: { hits: [], }, _scroll_id: lastScrollId, }); - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); await executeJob( 'job456', { headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null } }, cancellationToken ); - const lastCall = callWithRequestStub.getCall(callWithRequestStub.callCount - 1); - expect(lastCall.args[1]).toBe('clearScroll'); - expect(lastCall.args[2].scrollId).toEqual([lastScrollId]); + const lastCall = callAsCurrentUserStub.getCall(callAsCurrentUserStub.callCount - 1); + expect(lastCall.args[0]).toBe('clearScroll'); + expect(lastCall.args[1].scrollId).toEqual([lastScrollId]); }); it('calls clearScroll when there is an error iterating the hits', async function() { const lastScrollId = getRandomScrollId(); - callWithRequestStub.onFirstCall().resolves({ + callAsCurrentUserStub.onFirstCall().resolves({ hits: { hits: [ { @@ -341,7 +339,7 @@ describe('CSV Execute Job', function() { _scroll_id: lastScrollId, }); - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); const jobParams = { headers: encryptedHeaders, fields: ['one', 'two'], @@ -352,9 +350,9 @@ describe('CSV Execute Job', function() { executeJob('job123', jobParams, cancellationToken) ).rejects.toMatchInlineSnapshot(`[TypeError: Cannot read property 'indexOf' of undefined]`); - const lastCall = callWithRequestStub.getCall(callWithRequestStub.callCount - 1); - expect(lastCall.args[1]).toBe('clearScroll'); - expect(lastCall.args[2].scrollId).toEqual([lastScrollId]); + const lastCall = callAsCurrentUserStub.getCall(callAsCurrentUserStub.callCount - 1); + expect(lastCall.args[0]).toBe('clearScroll'); + expect(lastCall.args[1].scrollId).toEqual([lastScrollId]); }); }); @@ -364,14 +362,14 @@ describe('CSV Execute Job', function() { .config() .get.withArgs('xpack.reporting.csv.checkForFormulas') .returns(true); - callWithRequestStub.onFirstCall().returns({ + callAsCurrentUserStub.onFirstCall().returns({ hits: { hits: [{ _source: { one: '=SUM(A1:A2)', two: 'bar' } }], }, _scroll_id: 'scrollId', }); - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); const jobParams = { headers: encryptedHeaders, fields: ['one', 'two'], @@ -392,14 +390,14 @@ describe('CSV Execute Job', function() { .config() .get.withArgs('xpack.reporting.csv.checkForFormulas') .returns(true); - callWithRequestStub.onFirstCall().returns({ + callAsCurrentUserStub.onFirstCall().returns({ hits: { hits: [{ _source: { '=SUM(A1:A2)': 'foo', two: 'bar' } }], }, _scroll_id: 'scrollId', }); - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); const jobParams = { headers: encryptedHeaders, fields: ['=SUM(A1:A2)', 'two'], @@ -420,14 +418,14 @@ describe('CSV Execute Job', function() { .config() .get.withArgs('xpack.reporting.csv.checkForFormulas') .returns(true); - callWithRequestStub.onFirstCall().returns({ + callAsCurrentUserStub.onFirstCall().returns({ hits: { hits: [{ _source: { one: 'foo', two: 'bar' } }], }, _scroll_id: 'scrollId', }); - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); const jobParams = { headers: encryptedHeaders, fields: ['one', 'two'], @@ -448,14 +446,14 @@ describe('CSV Execute Job', function() { .config() .get.withArgs('xpack.reporting.csv.checkForFormulas') .returns(false); - callWithRequestStub.onFirstCall().returns({ + callAsCurrentUserStub.onFirstCall().returns({ hits: { hits: [{ _source: { one: '=SUM(A1:A2)', two: 'bar' } }], }, _scroll_id: 'scrollId', }); - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); const jobParams = { headers: encryptedHeaders, fields: ['one', 'two'], @@ -474,8 +472,8 @@ describe('CSV Execute Job', function() { describe('Elasticsearch call errors', function() { it('should reject Promise if search call errors out', async function() { - callWithRequestStub.rejects(new Error()); - const executeJob = executeJobFactory(mockServer, mockLogger); + callAsCurrentUserStub.rejects(new Error()); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); const jobParams = { headers: encryptedHeaders, fields: [], @@ -487,14 +485,14 @@ describe('CSV Execute Job', function() { }); it('should reject Promise if scroll call errors out', async function() { - callWithRequestStub.onFirstCall().resolves({ + callAsCurrentUserStub.onFirstCall().resolves({ hits: { hits: [{}], }, _scroll_id: 'scrollId', }); - callWithRequestStub.onSecondCall().rejects(new Error()); - const executeJob = executeJobFactory(mockServer, mockLogger); + callAsCurrentUserStub.onSecondCall().rejects(new Error()); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); const jobParams = { headers: encryptedHeaders, fields: [], @@ -508,14 +506,14 @@ describe('CSV Execute Job', function() { describe('invalid responses', function() { it('should reject Promise if search returns hits but no _scroll_id', async function() { - callWithRequestStub.resolves({ + callAsCurrentUserStub.resolves({ hits: { hits: [{}], }, _scroll_id: undefined, }); - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); const jobParams = { headers: encryptedHeaders, fields: [], @@ -529,14 +527,14 @@ describe('CSV Execute Job', function() { }); it('should reject Promise if search returns no hits and no _scroll_id', async function() { - callWithRequestStub.resolves({ + callAsCurrentUserStub.resolves({ hits: { hits: [], }, _scroll_id: undefined, }); - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); const jobParams = { headers: encryptedHeaders, fields: [], @@ -550,21 +548,21 @@ describe('CSV Execute Job', function() { }); it('should reject Promise if scroll returns hits but no _scroll_id', async function() { - callWithRequestStub.onFirstCall().resolves({ + callAsCurrentUserStub.onFirstCall().resolves({ hits: { hits: [{}], }, _scroll_id: 'scrollId', }); - callWithRequestStub.onSecondCall().resolves({ + callAsCurrentUserStub.onSecondCall().resolves({ hits: { hits: [{}], }, _scroll_id: undefined, }); - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); const jobParams = { headers: encryptedHeaders, fields: [], @@ -578,21 +576,21 @@ describe('CSV Execute Job', function() { }); it('should reject Promise if scroll returns no hits and no _scroll_id', async function() { - callWithRequestStub.onFirstCall().resolves({ + callAsCurrentUserStub.onFirstCall().resolves({ hits: { hits: [{}], }, _scroll_id: 'scrollId', }); - callWithRequestStub.onSecondCall().resolves({ + callAsCurrentUserStub.onSecondCall().resolves({ hits: { hits: [], }, _scroll_id: undefined, }); - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); const jobParams = { headers: encryptedHeaders, fields: [], @@ -610,23 +608,25 @@ describe('CSV Execute Job', function() { const scrollId = getRandomScrollId(); beforeEach(function() { - // We have to "re-stub" the callWithRequest stub here so that we can use the fakeFunction + // We have to "re-stub" the callAsCurrentUser stub here so that we can use the fakeFunction // that delays the Promise resolution so we have a chance to call cancellationToken.cancel(). // Otherwise, we get into an endless loop, and don't have a chance to call cancel - callWithRequestStub.restore(); - callWithRequestStub = sinon.stub(clusterStub, 'callWithRequest').callsFake(async function() { - await delay(1); - return { - hits: { - hits: [{}], - }, - _scroll_id: scrollId, - }; - }); + callAsCurrentUserStub.restore(); + callAsCurrentUserStub = sinon + .stub(clusterStub, 'callAsCurrentUser') + .callsFake(async function() { + await delay(1); + return { + hits: { + hits: [{}], + }, + _scroll_id: scrollId, + }; + }); }); it('should stop calling Elasticsearch when cancellationToken.cancel is called', async function() { - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); executeJob( 'job345', { headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null } }, @@ -634,14 +634,14 @@ describe('CSV Execute Job', function() { ); await delay(250); - const callCount = callWithRequestStub.callCount; + const callCount = callAsCurrentUserStub.callCount; cancellationToken.cancel(); await delay(250); - expect(callWithRequestStub.callCount).toBe(callCount + 1); // last call is to clear the scroll + expect(callAsCurrentUserStub.callCount).toBe(callCount + 1); // last call is to clear the scroll }); it(`shouldn't call clearScroll if it never got a scrollId`, async function() { - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); executeJob( 'job345', { headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null } }, @@ -649,13 +649,13 @@ describe('CSV Execute Job', function() { ); cancellationToken.cancel(); - for (let i = 0; i < callWithRequestStub.callCount; ++i) { - expect(callWithRequestStub.getCall(i).args[1]).to.not.be('clearScroll'); + for (let i = 0; i < callAsCurrentUserStub.callCount; ++i) { + expect(callAsCurrentUserStub.getCall(i).args[1]).to.not.be('clearScroll'); } }); it('should call clearScroll if it got a scrollId', async function() { - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); executeJob( 'job345', { headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null } }, @@ -665,15 +665,15 @@ describe('CSV Execute Job', function() { cancellationToken.cancel(); await delay(100); - const lastCall = callWithRequestStub.getCall(callWithRequestStub.callCount - 1); - expect(lastCall.args[1]).toBe('clearScroll'); - expect(lastCall.args[2].scrollId).toEqual([scrollId]); + const lastCall = callAsCurrentUserStub.getCall(callAsCurrentUserStub.callCount - 1); + expect(lastCall.args[0]).toBe('clearScroll'); + expect(lastCall.args[1].scrollId).toEqual([scrollId]); }); }); describe('csv content', function() { it('should write column headers to output, even if there are no results', async function() { - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); const jobParams = { headers: encryptedHeaders, fields: ['one', 'two'], @@ -685,7 +685,7 @@ describe('CSV Execute Job', function() { it('should use custom uiSettings csv:separator for header', async function() { uiSettingsGetStub.withArgs('csv:separator').returns(';'); - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); const jobParams = { headers: encryptedHeaders, fields: ['one', 'two'], @@ -697,7 +697,7 @@ describe('CSV Execute Job', function() { it('should escape column headers if uiSettings csv:quoteValues is true', async function() { uiSettingsGetStub.withArgs('csv:quoteValues').returns(true); - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); const jobParams = { headers: encryptedHeaders, fields: ['one and a half', 'two', 'three-and-four', 'five & six'], @@ -709,7 +709,7 @@ describe('CSV Execute Job', function() { it(`shouldn't escape column headers if uiSettings csv:quoteValues is false`, async function() { uiSettingsGetStub.withArgs('csv:quoteValues').returns(false); - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); const jobParams = { headers: encryptedHeaders, fields: ['one and a half', 'two', 'three-and-four', 'five & six'], @@ -720,8 +720,8 @@ describe('CSV Execute Job', function() { }); it('should write column headers to output, when there are results', async function() { - const executeJob = executeJobFactory(mockServer, mockLogger); - callWithRequestStub.onFirstCall().resolves({ + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); + callAsCurrentUserStub.onFirstCall().resolves({ hits: { hits: [{ one: '1', two: '2' }], }, @@ -740,8 +740,8 @@ describe('CSV Execute Job', function() { }); it('should use comma separated values of non-nested fields from _source', async function() { - const executeJob = executeJobFactory(mockServer, mockLogger); - callWithRequestStub.onFirstCall().resolves({ + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); + callAsCurrentUserStub.onFirstCall().resolves({ hits: { hits: [{ _source: { one: 'foo', two: 'bar' } }], }, @@ -761,14 +761,14 @@ describe('CSV Execute Job', function() { }); it('should concatenate the hits from multiple responses', async function() { - const executeJob = executeJobFactory(mockServer, mockLogger); - callWithRequestStub.onFirstCall().resolves({ + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); + callAsCurrentUserStub.onFirstCall().resolves({ hits: { hits: [{ _source: { one: 'foo', two: 'bar' } }], }, _scroll_id: 'scrollId', }); - callWithRequestStub.onSecondCall().resolves({ + callAsCurrentUserStub.onSecondCall().resolves({ hits: { hits: [{ _source: { one: 'baz', two: 'qux' } }], }, @@ -789,8 +789,8 @@ describe('CSV Execute Job', function() { }); it('should use field formatters to format fields', async function() { - const executeJob = executeJobFactory(mockServer, mockLogger); - callWithRequestStub.onFirstCall().resolves({ + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); + callAsCurrentUserStub.onFirstCall().resolves({ hits: { hits: [{ _source: { one: 'foo', two: 'bar' } }], }, @@ -834,7 +834,7 @@ describe('CSV Execute Job', function() { .get.withArgs('xpack.reporting.csv.maxSizeBytes') .returns(1); - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); const jobParams = { headers: encryptedHeaders, fields: ['one', 'two'], @@ -867,7 +867,7 @@ describe('CSV Execute Job', function() { .get.withArgs('xpack.reporting.csv.maxSizeBytes') .returns(9); - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); const jobParams = { headers: encryptedHeaders, fields: ['one', 'two'], @@ -900,14 +900,14 @@ describe('CSV Execute Job', function() { .get.withArgs('xpack.reporting.csv.maxSizeBytes') .returns(9); - callWithRequestStub.onFirstCall().returns({ + callAsCurrentUserStub.onFirstCall().returns({ hits: { hits: [{ _source: { one: 'foo', two: 'bar' } }], }, _scroll_id: 'scrollId', }); - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); const jobParams = { headers: encryptedHeaders, fields: ['one', 'two'], @@ -941,14 +941,14 @@ describe('CSV Execute Job', function() { .get.withArgs('xpack.reporting.csv.maxSizeBytes') .returns(18); - callWithRequestStub.onFirstCall().returns({ + callAsCurrentUserStub.onFirstCall().returns({ hits: { hits: [{ _source: { one: 'foo', two: 'bar' } }], }, _scroll_id: 'scrollId', }); - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); const jobParams = { headers: encryptedHeaders, fields: ['one', 'two'], @@ -981,14 +981,14 @@ describe('CSV Execute Job', function() { .get.withArgs('xpack.reporting.csv.scroll') .returns({ duration: scrollDuration }); - callWithRequestStub.onFirstCall().returns({ + callAsCurrentUserStub.onFirstCall().returns({ hits: { hits: [{}], }, _scroll_id: 'scrollId', }); - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); const jobParams = { headers: encryptedHeaders, fields: ['one', 'two'], @@ -998,9 +998,9 @@ describe('CSV Execute Job', function() { await executeJob('job123', jobParams, cancellationToken); - const searchCall = callWithRequestStub.firstCall; - expect(searchCall.args[1]).toBe('search'); - expect(searchCall.args[2].scroll).toBe(scrollDuration); + const searchCall = callAsCurrentUserStub.firstCall; + expect(searchCall.args[0]).toBe('search'); + expect(searchCall.args[1].scroll).toBe(scrollDuration); }); it('passes scroll size to initial search call', async function() { @@ -1010,14 +1010,14 @@ describe('CSV Execute Job', function() { .get.withArgs('xpack.reporting.csv.scroll') .returns({ size: scrollSize }); - callWithRequestStub.onFirstCall().resolves({ + callAsCurrentUserStub.onFirstCall().resolves({ hits: { hits: [{}], }, _scroll_id: 'scrollId', }); - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); const jobParams = { headers: encryptedHeaders, fields: ['one', 'two'], @@ -1027,9 +1027,9 @@ describe('CSV Execute Job', function() { await executeJob('job123', jobParams, cancellationToken); - const searchCall = callWithRequestStub.firstCall; - expect(searchCall.args[1]).toBe('search'); - expect(searchCall.args[2].size).toBe(scrollSize); + const searchCall = callAsCurrentUserStub.firstCall; + expect(searchCall.args[0]).toBe('search'); + expect(searchCall.args[1].size).toBe(scrollSize); }); it('passes scroll duration to subsequent scroll call', async function() { @@ -1039,14 +1039,14 @@ describe('CSV Execute Job', function() { .get.withArgs('xpack.reporting.csv.scroll') .returns({ duration: scrollDuration }); - callWithRequestStub.onFirstCall().resolves({ + callAsCurrentUserStub.onFirstCall().resolves({ hits: { hits: [{}], }, _scroll_id: 'scrollId', }); - const executeJob = executeJobFactory(mockServer, mockLogger); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, mockLogger); const jobParams = { headers: encryptedHeaders, fields: ['one', 'two'], @@ -1056,9 +1056,9 @@ describe('CSV Execute Job', function() { await executeJob('job123', jobParams, cancellationToken); - const scrollCall = callWithRequestStub.secondCall; - expect(scrollCall.args[1]).toBe('scroll'); - expect(scrollCall.args[2].scroll).toBe(scrollDuration); + const scrollCall = callAsCurrentUserStub.secondCall; + expect(scrollCall.args[0]).toBe('scroll'); + expect(scrollCall.args[1].scroll).toBe(scrollDuration); }); }); }); diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.ts b/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.ts index fe64fdc96d9043..280bbf13fa9928 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.ts +++ b/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.ts @@ -4,8 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import Hapi from 'hapi'; import { i18n } from '@kbn/i18n'; -import { KibanaRequest } from '../../../../../../../src/core/server'; +import { ElasticsearchServiceSetup, KibanaRequest } from '../../../../../../../src/core/server'; import { CSV_JOB_TYPE } from '../../../common/constants'; import { cryptoFactory } from '../../../server/lib'; import { ESQueueWorkerExecuteFn, ExecuteJobFactory, Logger, ServerFacade } from '../../../types'; @@ -15,8 +16,11 @@ import { createGenerateCsv } from './lib/generate_csv'; export const executeJobFactory: ExecuteJobFactory> = function executeJobFactoryFn(server: ServerFacade, parentLogger: Logger) { - const { callWithRequest } = server.plugins.elasticsearch.getCluster('data'); +>> = function executeJobFactoryFn( + server: ServerFacade, + elasticsearch: ElasticsearchServiceSetup, + parentLogger: Logger +) { const crypto = cryptoFactory(server); const config = server.config(); const logger = parentLogger.clone([CSV_JOB_TYPE, 'execute-job']); @@ -74,8 +78,11 @@ export const executeJobFactory: ExecuteJobFactory { - return callWithRequest(fakeRequest, endpoint, clientParams, options); + return callAsCurrentUser(endpoint, clientParams, options); }; const savedObjects = server.savedObjects; const savedObjectsClient = savedObjects.getScopedSavedObjectsClient( diff --git a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/create_job/create_job.ts b/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/create_job/create_job.ts index a270e3e0329fe8..ddef2aa0a62688 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/create_job/create_job.ts +++ b/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/create_job/create_job.ts @@ -6,24 +6,25 @@ import { notFound, notImplemented } from 'boom'; import { get } from 'lodash'; +import { ElasticsearchServiceSetup } from 'kibana/server'; import { CSV_FROM_SAVEDOBJECT_JOB_TYPE } from '../../../../common/constants'; import { cryptoFactory } from '../../../../server/lib'; import { CreateJobFactory, ImmediateCreateJobFn, - ServerFacade, - RequestFacade, Logger, + RequestFacade, + ServerFacade, } from '../../../../types'; import { + JobDocPayloadPanelCsv, + JobParamsPanelCsv, SavedObject, SavedObjectServiceError, SavedSearchObjectAttributesJSON, SearchPanel, TimeRangeParams, VisObjectAttributesJSON, - JobDocPayloadPanelCsv, - JobParamsPanelCsv, } from '../../types'; import { createJobSearch } from './create_job_search'; @@ -35,7 +36,11 @@ interface VisData { export const createJobFactory: CreateJobFactory> = function createJobFactoryFn(server: ServerFacade, parentLogger: Logger) { +>> = function createJobFactoryFn( + server: ServerFacade, + elasticsearch: ElasticsearchServiceSetup, + parentLogger: Logger +) { const crypto = cryptoFactory(server); const logger = parentLogger.clone([CSV_FROM_SAVEDOBJECT_JOB_TYPE, 'create-job']); diff --git a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/execute_job.ts b/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/execute_job.ts index 03f491deaa43d6..b1b7b7d818200e 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/execute_job.ts +++ b/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/execute_job.ts @@ -5,6 +5,7 @@ */ import { i18n } from '@kbn/i18n'; +import { ElasticsearchServiceSetup } from 'kibana/server'; import { CONTENT_TYPE_CSV, CSV_FROM_SAVEDOBJECT_JOB_TYPE } from '../../../common/constants'; import { cryptoFactory } from '../../../server/lib'; import { @@ -21,7 +22,11 @@ import { createGenerateCsv } from './lib'; export const executeJobFactory: ExecuteJobFactory> = function executeJobFactoryFn(server: ServerFacade, parentLogger: Logger) { +>> = function executeJobFactoryFn( + server: ServerFacade, + elasticsearch: ElasticsearchServiceSetup, + parentLogger: Logger +) { const crypto = cryptoFactory(server); const logger = parentLogger.clone([CSV_FROM_SAVEDOBJECT_JOB_TYPE, 'execute-job']); @@ -85,6 +90,7 @@ export const executeJobFactory: ExecuteJobFactory { export async function generateCsvSearch( req: RequestFacade, server: ServerFacade, + elasticsearch: ElasticsearchServiceSetup, logger: Logger, searchPanel: SearchPanel, jobParams: JobParamsDiscoverCsv @@ -152,8 +152,11 @@ export async function generateCsvSearch( sort: sortConfig, }, }; - const { callWithRequest } = server.plugins.elasticsearch.getCluster('data'); - const callCluster = (...params: [string, object]) => callWithRequest(req, ...params); + + const { callAsCurrentUser } = elasticsearch.dataClient.asScoped( + KibanaRequest.from(req.getRawRequest()) + ); + const callCluster = (...params: [string, object]) => callAsCurrentUser(...params); const config = server.config(); const uiSettings = await getUiSettings(uiConfig); diff --git a/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.test.js b/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.test.js index 4f02ab5d4c077e..bb33ef9c19a1dd 100644 --- a/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.test.js +++ b/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.test.js @@ -32,15 +32,6 @@ beforeEach(() => { info: { protocol: 'http', }, - plugins: { - elasticsearch: { - getCluster: memoize(() => { - return { - callWithRequest: jest.fn(), - }; - }), - }, - }, savedObjects: { getScopedSavedObjectsClient: jest.fn(), }, @@ -57,6 +48,12 @@ beforeEach(() => { afterEach(() => generatePngObservableFactory.mockReset()); +const mockElasticsearch = { + dataClient: { + asScoped: () => ({ callAsCurrentUser: jest.fn() }), + }, +}; + const getMockLogger = () => new LevelLogger(); const encryptHeaders = async headers => { @@ -70,7 +67,9 @@ test(`passes browserTimezone to generatePng`, async () => { const generatePngObservable = generatePngObservableFactory(); generatePngObservable.mockReturnValue(Rx.of(Buffer.from(''))); - const executeJob = executeJobFactory(mockServer, getMockLogger(), { browserDriverFactory: {} }); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, getMockLogger(), { + browserDriverFactory: {}, + }); const browserTimezone = 'UTC'; await executeJob( 'pngJobId', @@ -88,7 +87,9 @@ test(`passes browserTimezone to generatePng`, async () => { }); test(`returns content_type of application/png`, async () => { - const executeJob = executeJobFactory(mockServer, getMockLogger(), { browserDriverFactory: {} }); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, getMockLogger(), { + browserDriverFactory: {}, + }); const encryptedHeaders = await encryptHeaders({}); const generatePngObservable = generatePngObservableFactory(); @@ -108,7 +109,9 @@ test(`returns content of generatePng getBuffer base64 encoded`, async () => { const generatePngObservable = generatePngObservableFactory(); generatePngObservable.mockReturnValue(Rx.of(Buffer.from(testContent))); - const executeJob = executeJobFactory(mockServer, getMockLogger(), { browserDriverFactory: {} }); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, getMockLogger(), { + browserDriverFactory: {}, + }); const encryptedHeaders = await encryptHeaders({}); const { content } = await executeJob( 'pngJobId', diff --git a/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.ts b/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.ts index 7d5c69655c362f..c9f370197da662 100644 --- a/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.ts +++ b/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.ts @@ -5,6 +5,7 @@ */ import * as Rx from 'rxjs'; +import { ElasticsearchServiceSetup } from 'kibana/server'; import { catchError, map, mergeMap, takeUntil } from 'rxjs/operators'; import { PNG_JOB_TYPE } from '../../../../common/constants'; import { @@ -27,6 +28,7 @@ type QueuedPngExecutorFactory = ExecuteJobFactory { info: { protocol: 'http', }, - plugins: { - elasticsearch: { - getCluster: memoize(() => { - return { - callWithRequest: jest.fn(), - }; - }), - }, - }, savedObjects: { getScopedSavedObjectsClient: jest.fn(), }, @@ -58,6 +49,11 @@ beforeEach(() => { afterEach(() => generatePdfObservableFactory.mockReset()); const getMockLogger = () => new LevelLogger(); +const mockElasticsearch = { + dataClient: { + asScoped: () => ({ callAsCurrentUser: jest.fn() }), + }, +}; const encryptHeaders = async headers => { const crypto = cryptoFactory(mockServer); @@ -70,7 +66,9 @@ test(`passes browserTimezone to generatePdf`, async () => { const generatePdfObservable = generatePdfObservableFactory(); generatePdfObservable.mockReturnValue(Rx.of(Buffer.from(''))); - const executeJob = executeJobFactory(mockServer, getMockLogger(), { browserDriverFactory: {} }); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, getMockLogger(), { + browserDriverFactory: {}, + }); const browserTimezone = 'UTC'; await executeJob( 'pdfJobId', @@ -91,7 +89,9 @@ test(`passes browserTimezone to generatePdf`, async () => { }); test(`returns content_type of application/pdf`, async () => { - const executeJob = executeJobFactory(mockServer, getMockLogger(), { browserDriverFactory: {} }); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, getMockLogger(), { + browserDriverFactory: {}, + }); const encryptedHeaders = await encryptHeaders({}); const generatePdfObservable = generatePdfObservableFactory(); @@ -111,7 +111,9 @@ test(`returns content of generatePdf getBuffer base64 encoded`, async () => { const generatePdfObservable = generatePdfObservableFactory(); generatePdfObservable.mockReturnValue(Rx.of(Buffer.from(testContent))); - const executeJob = executeJobFactory(mockServer, getMockLogger(), { browserDriverFactory: {} }); + const executeJob = executeJobFactory(mockServer, mockElasticsearch, getMockLogger(), { + browserDriverFactory: {}, + }); const encryptedHeaders = await encryptHeaders({}); const { content } = await executeJob( 'pdfJobId', diff --git a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.ts b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.ts index dee53697c6681d..162376e31216e0 100644 --- a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.ts +++ b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.ts @@ -5,6 +5,7 @@ */ import * as Rx from 'rxjs'; +import { ElasticsearchServiceSetup } from 'kibana/server'; import { catchError, map, mergeMap, takeUntil } from 'rxjs/operators'; import { ServerFacade, @@ -28,6 +29,7 @@ type QueuedPdfExecutorFactory = ExecuteJobFactory { @@ -74,10 +69,6 @@ export const reporting = (kibana: any) => { async init(server: Legacy.Server) { const coreSetup = server.newPlatform.setup.core; - const pluginsSetup: ReportingSetupDeps = { - security: server.newPlatform.setup.plugins.security as SecurityPluginSetup, - usageCollection: server.newPlatform.setup.plugins.usageCollection, - }; const fieldFormatServiceFactory = async (uiSettings: IUiSettingsClient) => { const [, plugins] = await coreSetup.getStartServices(); @@ -90,18 +81,22 @@ export const reporting = (kibana: any) => { config: server.config, info: server.info, route: server.route.bind(server), - plugins: { - elasticsearch: server.plugins.elasticsearch, - xpack_main: server.plugins.xpack_main, - }, + plugins: { xpack_main: server.plugins.xpack_main }, savedObjects: server.savedObjects, fieldFormatServiceFactory, uiSettingsServiceFactory: server.uiSettingsServiceFactory, }; - const initializerContext = server.newPlatform.coreContext; - const plugin: ReportingPlugin = reportingPluginFactory(initializerContext, __LEGACY, this); - await plugin.setup(coreSetup, pluginsSetup); + const plugin: ReportingPlugin = reportingPluginFactory( + server.newPlatform.coreContext, + __LEGACY, + this + ); + await plugin.setup(coreSetup, { + elasticsearch: coreSetup.elasticsearch, + security: server.newPlatform.setup.plugins.security as SecurityPluginSetup, + usageCollection: server.newPlatform.setup.plugins.usageCollection, + }); }, deprecations({ unused }: any) { diff --git a/x-pack/legacy/plugins/reporting/server/lib/create_queue.ts b/x-pack/legacy/plugins/reporting/server/lib/create_queue.ts index 05b760c0c3bd6c..c4e32b3ebcd99e 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/create_queue.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/create_queue.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ElasticsearchServiceSetup } from 'kibana/server'; import { ServerFacade, ExportTypesRegistry, @@ -23,6 +24,7 @@ interface CreateQueueFactoryOpts { export function createQueueFactory( server: ServerFacade, + elasticsearch: ElasticsearchServiceSetup, logger: Logger, { exportTypesRegistry, browserDriverFactory }: CreateQueueFactoryOpts ): Esqueue { @@ -33,7 +35,7 @@ export function createQueueFactory( interval: queueConfig.indexInterval, timeout: queueConfig.timeout, dateSeparator: '.', - client: server.plugins.elasticsearch.getCluster('admin'), + client: elasticsearch.dataClient, logger: createTaggedLogger(logger, ['esqueue', 'queue-worker']), }; @@ -41,7 +43,7 @@ export function createQueueFactory( if (queueConfig.pollEnabled) { // create workers to poll the index for idle jobs waiting to be claimed and executed - const createWorker = createWorkerFactory(server, logger, { + const createWorker = createWorkerFactory(server, elasticsearch, logger, { exportTypesRegistry, browserDriverFactory, }); diff --git a/x-pack/legacy/plugins/reporting/server/lib/create_worker.test.ts b/x-pack/legacy/plugins/reporting/server/lib/create_worker.test.ts index 6a5c93db32376a..f5c42e5505cd1d 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/create_worker.test.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/create_worker.test.ts @@ -5,13 +5,14 @@ */ import * as sinon from 'sinon'; -import { ServerFacade, HeadlessChromiumDriverFactory } from '../../types'; -import { ExportTypesRegistry } from './export_types_registry'; +import { ElasticsearchServiceSetup } from 'kibana/server'; +import { HeadlessChromiumDriverFactory, ServerFacade } from '../../types'; import { createWorkerFactory } from './create_worker'; // @ts-ignore import { Esqueue } from './esqueue'; // @ts-ignore import { ClientMock } from './esqueue/__tests__/fixtures/legacy_elasticsearch'; +import { ExportTypesRegistry } from './export_types_registry'; const configGetStub = sinon.stub(); configGetStub.withArgs('xpack.reporting.queue').returns({ @@ -48,10 +49,15 @@ describe('Create Worker', () => { test('Creates a single Esqueue worker for Reporting', async () => { const exportTypesRegistry = getMockExportTypesRegistry(); - const createWorker = createWorkerFactory(getMockServer(), getMockLogger(), { - exportTypesRegistry: exportTypesRegistry as ExportTypesRegistry, - browserDriverFactory: {} as HeadlessChromiumDriverFactory, - }); + const createWorker = createWorkerFactory( + getMockServer(), + {} as ElasticsearchServiceSetup, + getMockLogger(), + { + exportTypesRegistry: exportTypesRegistry as ExportTypesRegistry, + browserDriverFactory: {} as HeadlessChromiumDriverFactory, + } + ); const registerWorkerSpy = sinon.spy(queue, 'registerWorker'); createWorker(queue); @@ -82,10 +88,15 @@ Object { { executeJobFactory: executeJobFactoryStub }, { executeJobFactory: executeJobFactoryStub }, ]); - const createWorker = createWorkerFactory(getMockServer(), getMockLogger(), { - exportTypesRegistry: exportTypesRegistry as ExportTypesRegistry, - browserDriverFactory: {} as HeadlessChromiumDriverFactory, - }); + const createWorker = createWorkerFactory( + getMockServer(), + {} as ElasticsearchServiceSetup, + getMockLogger(), + { + exportTypesRegistry: exportTypesRegistry as ExportTypesRegistry, + browserDriverFactory: {} as HeadlessChromiumDriverFactory, + } + ); const registerWorkerSpy = sinon.spy(queue, 'registerWorker'); createWorker(queue); diff --git a/x-pack/legacy/plugins/reporting/server/lib/create_worker.ts b/x-pack/legacy/plugins/reporting/server/lib/create_worker.ts index 67869016a250be..2ca638f641291d 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/create_worker.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/create_worker.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ElasticsearchServiceSetup } from 'kibana/server'; import { PLUGIN_ID } from '../../common/constants'; import { ExportTypesRegistry, HeadlessChromiumDriverFactory } from '../../types'; import { CancellationToken } from '../../common/cancellation_token'; @@ -29,6 +30,7 @@ interface CreateWorkerFactoryOpts { export function createWorkerFactory( server: ServerFacade, + elasticsearch: ElasticsearchServiceSetup, logger: Logger, { exportTypesRegistry, browserDriverFactory }: CreateWorkerFactoryOpts ) { @@ -50,7 +52,9 @@ export function createWorkerFactory( ExportTypeDefinition >) { // TODO: the executeJobFn should be unwrapped in the register method of the export types registry - const jobExecutor = exportType.executeJobFactory(server, logger, { browserDriverFactory }); + const jobExecutor = exportType.executeJobFactory(server, elasticsearch, logger, { + browserDriverFactory, + }); jobExecutors.set(exportType.jobType, jobExecutor); } diff --git a/x-pack/legacy/plugins/reporting/server/lib/enqueue_job.ts b/x-pack/legacy/plugins/reporting/server/lib/enqueue_job.ts index 14c57fa35dcf4e..1da8a3795aacc5 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/enqueue_job.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/enqueue_job.ts @@ -5,6 +5,7 @@ */ import { get } from 'lodash'; +import { ElasticsearchServiceSetup } from 'kibana/server'; // @ts-ignore import { events as esqueueEvents } from './esqueue'; import { @@ -35,6 +36,7 @@ interface EnqueueJobFactoryOpts { export function enqueueJobFactory( server: ServerFacade, + elasticsearch: ElasticsearchServiceSetup, parentLogger: Logger, { exportTypesRegistry, esqueue }: EnqueueJobFactoryOpts ): EnqueueJobFn { @@ -61,7 +63,7 @@ export function enqueueJobFactory( } // TODO: the createJobFn should be unwrapped in the register method of the export types registry - const createJob = exportType.createJobFactory(server, logger) as CreateJobFn; + const createJob = exportType.createJobFactory(server, elasticsearch, logger) as CreateJobFn; const payload = await createJob(jobParams, headers, request); const options = { diff --git a/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/fixtures/legacy_elasticsearch.js b/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/fixtures/legacy_elasticsearch.js index 31bdf7767983dd..ebda7ff955b11c 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/fixtures/legacy_elasticsearch.js +++ b/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/fixtures/legacy_elasticsearch.js @@ -1,10 +1,14 @@ -import { uniqueId, times, random } from 'lodash'; -import * as legacyElasticsearch from 'elasticsearch'; +/* + * 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 { constants } from '../../constants'; +import { uniqueId, times, random } from 'lodash'; +import { errors as esErrors } from 'elasticsearch'; export function ClientMock() { - this.callWithInternalUser = (endpoint, params = {}, ...rest) => { + this.callAsInternalUser = (endpoint, params = {}, ...rest) => { if (endpoint === 'indices.create') { return Promise.resolve({ acknowledged: true }); } @@ -21,12 +25,12 @@ export function ClientMock() { _seq_no: 1, _primary_term: 1, _shards: { total: shardCount, successful: shardCount, failed: 0 }, - created: true + created: true, }); } if (endpoint === 'get') { - if (params === legacyElasticsearch.errors.NotFound) return legacyElasticsearch.errors.NotFound; + if (params === esErrors.NotFound) return esErrors.NotFound; const _source = { jobtype: 'jobtype', @@ -34,7 +38,7 @@ export function ClientMock() { payload: { id: 'sample-job-1', - now: 'Mon Apr 25 2016 14:13:04 GMT-0700 (MST)' + now: 'Mon Apr 25 2016 14:13:04 GMT-0700 (MST)', }, priority: 10, @@ -43,7 +47,7 @@ export function ClientMock() { attempts: 0, max_attempts: 3, status: 'pending', - ...(rest[0] || {}) + ...(rest[0] || {}), }; return Promise.resolve({ @@ -52,7 +56,7 @@ export function ClientMock() { _seq_no: params._seq_no || 1, _primary_term: params._primary_term || 1, found: true, - _source: _source + _source: _source, }); } @@ -68,8 +72,8 @@ export function ClientMock() { _source: { created_at: new Date().toString(), number: random(0, count, true), - ...source - } + ...source, + }, }; }); return Promise.resolve({ @@ -78,13 +82,13 @@ export function ClientMock() { _shards: { total: 5, successful: 5, - failed: 0 + failed: 0, }, hits: { total: count, max_score: null, - hits: hits - } + hits: hits, + }, }); } @@ -96,7 +100,7 @@ export function ClientMock() { _seq_no: params.if_seq_no + 1 || 2, _primary_term: params.if_primary_term + 1 || 2, _shards: { total: shardCount, successful: shardCount, failed: 0 }, - created: true + created: true, }); } diff --git a/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/helpers/create_index.js b/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/helpers/create_index.js index 23e9aab5bad115..2944574534a827 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/helpers/create_index.js +++ b/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/helpers/create_index.js @@ -17,7 +17,7 @@ describe('Create Index', function() { beforeEach(function() { client = new ClientMock(); - createSpy = sinon.spy(client, 'callWithInternalUser').withArgs('indices.create'); + createSpy = sinon.spy(client, 'callAsInternalUser').withArgs('indices.create'); }); it('should return true', function() { @@ -75,10 +75,10 @@ describe('Create Index', function() { beforeEach(function() { client = new ClientMock(); sinon - .stub(client, 'callWithInternalUser') + .stub(client, 'callAsInternalUser') .withArgs('indices.exists') .callsFake(() => Promise.resolve(true)); - createSpy = client.callWithInternalUser.withArgs('indices.create'); + createSpy = client.callAsInternalUser.withArgs('indices.create'); }); it('should return true', function() { diff --git a/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/index.js b/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/index.js index 8f1ed69de5e7f6..428c0f0bc0736c 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/index.js +++ b/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/index.js @@ -40,7 +40,7 @@ describe('Esqueue class', function() { describe('Queue construction', function() { it('should ping the ES server', function() { - const pingSpy = sinon.spy(client, 'callWithInternalUser').withArgs('ping'); + const pingSpy = sinon.spy(client, 'callAsInternalUser').withArgs('ping'); new Esqueue('esqueue', { client }); sinon.assert.calledOnce(pingSpy); }); diff --git a/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/job.js b/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/job.js index 2d8410c18ddeab..c7812ec151b005 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/job.js +++ b/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/job.js @@ -79,7 +79,7 @@ describe('Job Class', function() { beforeEach(function() { type = 'type1'; payload = { id: '123' }; - indexSpy = sinon.spy(client, 'callWithInternalUser').withArgs('index'); + indexSpy = sinon.spy(client, 'callAsInternalUser').withArgs('index'); }); it('should create the target index', function() { @@ -121,7 +121,7 @@ describe('Job Class', function() { }); it('should refresh the index', function() { - const refreshSpy = client.callWithInternalUser.withArgs('indices.refresh'); + const refreshSpy = client.callAsInternalUser.withArgs('indices.refresh'); const job = new Job(mockQueue, index, type, payload); return job.ready.then(() => { @@ -165,9 +165,9 @@ describe('Job Class', function() { it('should emit error on client index failure', function(done) { const errMsg = 'test document index failure'; - client.callWithInternalUser.restore(); + client.callAsInternalUser.restore(); sinon - .stub(client, 'callWithInternalUser') + .stub(client, 'callAsInternalUser') .withArgs('index') .callsFake(() => Promise.reject(new Error(errMsg))); const job = new Job(mockQueue, index, type, payload); @@ -215,7 +215,7 @@ describe('Job Class', function() { beforeEach(function() { type = 'type1'; payload = { id: '123' }; - indexSpy = sinon.spy(client, 'callWithInternalUser').withArgs('index'); + indexSpy = sinon.spy(client, 'callAsInternalUser').withArgs('index'); }); it('should set attempt count to 0', function() { @@ -281,7 +281,7 @@ describe('Job Class', function() { authorization: 'Basic cXdlcnR5', }, }; - indexSpy = sinon.spy(client, 'callWithInternalUser').withArgs('index'); + indexSpy = sinon.spy(client, 'callAsInternalUser').withArgs('index'); }); it('should index the created_by value', function() { @@ -367,10 +367,10 @@ describe('Job Class', function() { }; const job = new Job(mockQueue, index, type, payload, optionals); - return Promise.resolve(client.callWithInternalUser('get', {}, optionals)) + return Promise.resolve(client.callAsInternalUser('get', {}, optionals)) .then(doc => { sinon - .stub(client, 'callWithInternalUser') + .stub(client, 'callAsInternalUser') .withArgs('get') .returns(Promise.resolve(doc)); }) diff --git a/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/worker.js b/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/worker.js index bd82a9e9f99db9..ad93a1882746dc 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/worker.js +++ b/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/worker.js @@ -288,7 +288,7 @@ describe('Worker class', function() { describe('error handling', function() { it('should pass search errors', function(done) { searchStub = sinon - .stub(mockQueue.client, 'callWithInternalUser') + .stub(mockQueue.client, 'callAsInternalUser') .withArgs('search') .callsFake(() => Promise.reject()); worker = new Worker(mockQueue, 'test', noop, defaultWorkerOptions); @@ -303,7 +303,7 @@ describe('Worker class', function() { describe('missing index', function() { it('should swallow error', function(done) { searchStub = sinon - .stub(mockQueue.client, 'callWithInternalUser') + .stub(mockQueue.client, 'callAsInternalUser') .withArgs('search') .callsFake(() => Promise.reject({ status: 404 })); worker = new Worker(mockQueue, 'test', noop, defaultWorkerOptions); @@ -317,7 +317,7 @@ describe('Worker class', function() { it('should return an empty array', function(done) { searchStub = sinon - .stub(mockQueue.client, 'callWithInternalUser') + .stub(mockQueue.client, 'callAsInternalUser') .withArgs('search') .callsFake(() => Promise.reject({ status: 404 })); worker = new Worker(mockQueue, 'test', noop, defaultWorkerOptions); @@ -343,7 +343,7 @@ describe('Worker class', function() { beforeEach(() => { searchStub = sinon - .stub(mockQueue.client, 'callWithInternalUser') + .stub(mockQueue.client, 'callAsInternalUser') .withArgs('search') .callsFake(() => Promise.resolve({ hits: { hits: [] } })); anchorMoment = moment(anchor); @@ -417,10 +417,10 @@ describe('Worker class', function() { type: 'test', id: 12345, }; - return mockQueue.client.callWithInternalUser('get', params).then(jobDoc => { + return mockQueue.client.callAsInternalUser('get', params).then(jobDoc => { job = jobDoc; worker = new Worker(mockQueue, 'test', noop, defaultWorkerOptions); - updateSpy = sinon.spy(mockQueue.client, 'callWithInternalUser').withArgs('update'); + updateSpy = sinon.spy(mockQueue.client, 'callAsInternalUser').withArgs('update'); }); }); @@ -483,9 +483,9 @@ describe('Worker class', function() { }); it('should reject the promise on conflict errors', function() { - mockQueue.client.callWithInternalUser.restore(); + mockQueue.client.callAsInternalUser.restore(); sinon - .stub(mockQueue.client, 'callWithInternalUser') + .stub(mockQueue.client, 'callAsInternalUser') .withArgs('update') .returns(Promise.reject({ statusCode: 409 })); return worker._claimJob(job).catch(err => { @@ -494,9 +494,9 @@ describe('Worker class', function() { }); it('should reject the promise on other errors', function() { - mockQueue.client.callWithInternalUser.restore(); + mockQueue.client.callAsInternalUser.restore(); sinon - .stub(mockQueue.client, 'callWithInternalUser') + .stub(mockQueue.client, 'callAsInternalUser') .withArgs('update') .returns(Promise.reject({ statusCode: 401 })); return worker._claimJob(job).catch(err => { @@ -532,12 +532,12 @@ describe('Worker class', function() { }); afterEach(() => { - mockQueue.client.callWithInternalUser.restore(); + mockQueue.client.callAsInternalUser.restore(); }); it('should emit for errors from claiming job', function(done) { sinon - .stub(mockQueue.client, 'callWithInternalUser') + .stub(mockQueue.client, 'callAsInternalUser') .withArgs('update') .rejects({ statusCode: 401 }); @@ -558,7 +558,7 @@ describe('Worker class', function() { it('should reject the promise if an error claiming the job', function() { sinon - .stub(mockQueue.client, 'callWithInternalUser') + .stub(mockQueue.client, 'callAsInternalUser') .withArgs('update') .rejects({ statusCode: 409 }); return worker._claimPendingJobs(getMockJobs()).catch(err => { @@ -568,7 +568,7 @@ describe('Worker class', function() { it('should get the pending job', function() { sinon - .stub(mockQueue.client, 'callWithInternalUser') + .stub(mockQueue.client, 'callAsInternalUser') .withArgs('update') .resolves({ test: 'cool' }); sinon.stub(worker, '_performJob').callsFake(identity); @@ -590,10 +590,10 @@ describe('Worker class', function() { anchorMoment = moment(anchor); clock = sinon.useFakeTimers(anchorMoment.valueOf()); - return mockQueue.client.callWithInternalUser('get').then(jobDoc => { + return mockQueue.client.callAsInternalUser('get').then(jobDoc => { job = jobDoc; worker = new Worker(mockQueue, 'test', noop, defaultWorkerOptions); - updateSpy = sinon.spy(mockQueue.client, 'callWithInternalUser').withArgs('update'); + updateSpy = sinon.spy(mockQueue.client, 'callAsInternalUser').withArgs('update'); }); }); @@ -625,18 +625,18 @@ describe('Worker class', function() { }); it('should return true on conflict errors', function() { - mockQueue.client.callWithInternalUser.restore(); + mockQueue.client.callAsInternalUser.restore(); sinon - .stub(mockQueue.client, 'callWithInternalUser') + .stub(mockQueue.client, 'callAsInternalUser') .withArgs('update') .rejects({ statusCode: 409 }); return worker._failJob(job).then(res => expect(res).to.equal(true)); }); it('should return false on other document update errors', function() { - mockQueue.client.callWithInternalUser.restore(); + mockQueue.client.callAsInternalUser.restore(); sinon - .stub(mockQueue.client, 'callWithInternalUser') + .stub(mockQueue.client, 'callAsInternalUser') .withArgs('update') .rejects({ statusCode: 401 }); return worker._failJob(job).then(res => expect(res).to.equal(false)); @@ -672,9 +672,9 @@ describe('Worker class', function() { }); it('should emit on other document update errors', function(done) { - mockQueue.client.callWithInternalUser.restore(); + mockQueue.client.callAsInternalUser.restore(); sinon - .stub(mockQueue.client, 'callWithInternalUser') + .stub(mockQueue.client, 'callAsInternalUser') .withArgs('update') .rejects({ statusCode: 401 }); @@ -703,9 +703,9 @@ describe('Worker class', function() { value: random(0, 100, true), }; - return mockQueue.client.callWithInternalUser('get', {}, { payload }).then(jobDoc => { + return mockQueue.client.callAsInternalUser('get', {}, { payload }).then(jobDoc => { job = jobDoc; - updateSpy = sinon.spy(mockQueue.client, 'callWithInternalUser').withArgs('update'); + updateSpy = sinon.spy(mockQueue.client, 'callAsInternalUser').withArgs('update'); }); }); @@ -871,7 +871,7 @@ describe('Worker class', function() { }; sinon - .stub(mockQueue.client, 'callWithInternalUser') + .stub(mockQueue.client, 'callAsInternalUser') .withArgs('update') .rejects({ statusCode: 413 }); @@ -893,7 +893,7 @@ describe('Worker class', function() { describe('search failure', function() { it('causes _processPendingJobs to reject the Promise', function() { sinon - .stub(mockQueue.client, 'callWithInternalUser') + .stub(mockQueue.client, 'callAsInternalUser') .withArgs('search') .rejects(new Error('test error')); worker = new Worker(mockQueue, 'test', noop, defaultWorkerOptions); @@ -996,7 +996,7 @@ describe('Worker class', function() { beforeEach(function() { sinon - .stub(mockQueue.client, 'callWithInternalUser') + .stub(mockQueue.client, 'callAsInternalUser') .withArgs('search') .callsFake(() => Promise.resolve({ hits: { hits: [] } })); }); @@ -1086,20 +1086,12 @@ describe('Format Job Object', () => { }); }); -// FAILING: https://github.com/elastic/kibana/issues/51372 -describe.skip('Get Doc Path from ES Response', () => { +describe('Get Doc Path from ES Response', () => { it('returns a formatted string after response of an update', function() { const responseMock = { _index: 'foo', _id: 'booId', }; - expect(getUpdatedDocPath(responseMock)).equal('/foo/_doc/booId'); - }); - it('returns the same formatted string even if there is no _doc provided', function() { - const responseMock = { - _index: 'foo', - _id: 'booId', - }; - expect(getUpdatedDocPath(responseMock)).equal('/foo/_doc/booId'); + expect(getUpdatedDocPath(responseMock)).equal('/foo/booId'); }); }); diff --git a/x-pack/legacy/plugins/reporting/server/lib/esqueue/helpers/create_index.js b/x-pack/legacy/plugins/reporting/server/lib/esqueue/helpers/create_index.js index 670c2907fb8322..465f27a817ba75 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/esqueue/helpers/create_index.js +++ b/x-pack/legacy/plugins/reporting/server/lib/esqueue/helpers/create_index.js @@ -78,13 +78,13 @@ export function createIndex(client, indexName, indexSettings = {}) { }; return client - .callWithInternalUser('indices.exists', { + .callAsInternalUser('indices.exists', { index: indexName, }) .then(exists => { if (!exists) { return client - .callWithInternalUser('indices.create', { + .callAsInternalUser('indices.create', { index: indexName, body: body, }) diff --git a/x-pack/legacy/plugins/reporting/server/lib/esqueue/index.js b/x-pack/legacy/plugins/reporting/server/lib/esqueue/index.js index b42ef84168940d..bd30ca9ae0f29e 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/esqueue/index.js +++ b/x-pack/legacy/plugins/reporting/server/lib/esqueue/index.js @@ -32,7 +32,7 @@ export class Esqueue extends EventEmitter { } _initTasks() { - const initTasks = [this.client.callWithInternalUser('ping')]; + const initTasks = [this.client.callAsInternalUser('ping')]; return Promise.all(initTasks).catch(err => { this._logger(['initTasks', 'error'], err); diff --git a/x-pack/legacy/plugins/reporting/server/lib/esqueue/job.js b/x-pack/legacy/plugins/reporting/server/lib/esqueue/job.js index a7d8f4df3fd541..826fcf360a4ca2 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/esqueue/job.js +++ b/x-pack/legacy/plugins/reporting/server/lib/esqueue/job.js @@ -78,7 +78,7 @@ export class Job extends events.EventEmitter { } this.ready = createIndex(this._client, this.index, this.indexSettings) - .then(() => this._client.callWithInternalUser('index', indexParams)) + .then(() => this._client.callAsInternalUser('index', indexParams)) .then(doc => { this.document = { id: doc._id, @@ -89,7 +89,7 @@ export class Job extends events.EventEmitter { this.debug(`Job created in index ${this.index}`); return this._client - .callWithInternalUser('indices.refresh', { + .callAsInternalUser('indices.refresh', { index: this.index, }) .then(() => { @@ -111,7 +111,7 @@ export class Job extends events.EventEmitter { get() { return this.ready .then(() => { - return this._client.callWithInternalUser('get', { + return this._client.callAsInternalUser('get', { index: this.index, id: this.id, }); diff --git a/x-pack/legacy/plugins/reporting/server/lib/esqueue/worker.js b/x-pack/legacy/plugins/reporting/server/lib/esqueue/worker.js index 7ad84460a0c459..43735979422783 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/esqueue/worker.js +++ b/x-pack/legacy/plugins/reporting/server/lib/esqueue/worker.js @@ -160,7 +160,7 @@ export class Worker extends events.EventEmitter { }; return this._client - .callWithInternalUser('update', { + .callAsInternalUser('update', { index: job._index, id: job._id, if_seq_no: job._seq_no, @@ -199,7 +199,7 @@ export class Worker extends events.EventEmitter { }); return this._client - .callWithInternalUser('update', { + .callAsInternalUser('update', { index: job._index, id: job._id, if_seq_no: job._seq_no, @@ -286,7 +286,7 @@ export class Worker extends events.EventEmitter { }; return this._client - .callWithInternalUser('update', { + .callAsInternalUser('update', { index: job._index, id: job._id, if_seq_no: job._seq_no, @@ -431,7 +431,7 @@ export class Worker extends events.EventEmitter { }; return this._client - .callWithInternalUser('search', { + .callAsInternalUser('search', { index: `${this.queue.index}-*`, body: query, }) diff --git a/x-pack/legacy/plugins/reporting/server/lib/jobs_query.ts b/x-pack/legacy/plugins/reporting/server/lib/jobs_query.ts index 0c16f780c34acd..3562834230ea1d 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/jobs_query.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/jobs_query.ts @@ -4,9 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ +import { errors as elasticsearchErrors } from 'elasticsearch'; +import { ElasticsearchServiceSetup } from 'kibana/server'; import { get } from 'lodash'; -import { ServerFacade, JobSource } from '../../types'; +import { JobSource, ServerFacade } from '../../types'; +const esErrors = elasticsearchErrors as Record; const defaultSize = 10; interface QueryBody { @@ -34,12 +37,9 @@ interface CountAggResult { count: number; } -export function jobsQueryFactory(server: ServerFacade) { +export function jobsQueryFactory(server: ServerFacade, elasticsearch: ElasticsearchServiceSetup) { const index = server.config().get('xpack.reporting.index'); - // @ts-ignore `errors` does not exist on type Cluster - const { callWithInternalUser, errors: esErrors } = server.plugins.elasticsearch.getCluster( - 'admin' - ); + const { callAsInternalUser } = elasticsearch.adminClient; function getUsername(user: any) { return get(user, 'username', false); @@ -61,7 +61,7 @@ export function jobsQueryFactory(server: ServerFacade) { body: Object.assign(defaultBody[queryType] || {}, body), }; - return callWithInternalUser(queryType, query).catch(err => { + return callAsInternalUser(queryType, query).catch(err => { if (err instanceof esErrors['401']) return; if (err instanceof esErrors['403']) return; if (err instanceof esErrors['404']) return; diff --git a/x-pack/legacy/plugins/reporting/server/lib/validate/index.ts b/x-pack/legacy/plugins/reporting/server/lib/validate/index.ts index 79a64bd82d0228..028d8fa143487c 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/validate/index.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/validate/index.ts @@ -5,7 +5,8 @@ */ import { i18n } from '@kbn/i18n'; -import { ServerFacade, Logger } from '../../../types'; +import { ElasticsearchServiceSetup } from 'kibana/server'; +import { Logger, ServerFacade } from '../../../types'; import { HeadlessChromiumDriverFactory } from '../../browsers/chromium/driver_factory'; import { validateBrowser } from './validate_browser'; import { validateEncryptionKey } from './validate_encryption_key'; @@ -14,6 +15,7 @@ import { validateServerHost } from './validate_server_host'; export async function runValidations( server: ServerFacade, + elasticsearch: ElasticsearchServiceSetup, logger: Logger, browserFactory: HeadlessChromiumDriverFactory ) { @@ -21,7 +23,7 @@ export async function runValidations( await Promise.all([ validateBrowser(server, browserFactory, logger), validateEncryptionKey(server, logger), - validateMaxContentLength(server, logger), + validateMaxContentLength(server, elasticsearch, logger), validateServerHost(server), ]); logger.debug( diff --git a/x-pack/legacy/plugins/reporting/server/lib/validate/__tests__/validate_max_content_length.js b/x-pack/legacy/plugins/reporting/server/lib/validate/validate_max_content_length.test.js similarity index 65% rename from x-pack/legacy/plugins/reporting/server/lib/validate/__tests__/validate_max_content_length.js rename to x-pack/legacy/plugins/reporting/server/lib/validate/validate_max_content_length.test.js index 48a58618f34cc7..942dcaf842696c 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/validate/__tests__/validate_max_content_length.js +++ b/x-pack/legacy/plugins/reporting/server/lib/validate/validate_max_content_length.test.js @@ -3,14 +3,26 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; + import sinon from 'sinon'; -import { validateMaxContentLength } from '../validate_max_content_length'; +import { validateMaxContentLength } from './validate_max_content_length'; const FIVE_HUNDRED_MEGABYTES = 524288000; const ONE_HUNDRED_MEGABYTES = 104857600; describe('Reporting: Validate Max Content Length', () => { + const elasticsearch = { + dataClient: { + callAsInternalUser: () => ({ + defaults: { + http: { + max_content_length: '100mb', + }, + }, + }), + }, + }; + const logger = { warning: sinon.spy(), }; @@ -24,22 +36,20 @@ describe('Reporting: Validate Max Content Length', () => { config: () => ({ get: sinon.stub().returns(FIVE_HUNDRED_MEGABYTES), }), - plugins: { - elasticsearch: { - getCluster: () => ({ - callWithInternalUser: () => ({ - defaults: { - http: { - max_content_length: '100mb', - }, - }, - }), - }), - }, + }; + const elasticsearch = { + dataClient: { + callAsInternalUser: () => ({ + defaults: { + http: { + max_content_length: '100mb', + }, + }, + }), }, }; - await validateMaxContentLength(server, logger); + await validateMaxContentLength(server, elasticsearch, logger); sinon.assert.calledWithMatch( logger.warning, @@ -64,22 +74,11 @@ describe('Reporting: Validate Max Content Length', () => { config: () => ({ get: sinon.stub().returns(ONE_HUNDRED_MEGABYTES), }), - plugins: { - elasticsearch: { - getCluster: () => ({ - callWithInternalUser: () => ({ - defaults: { - http: { - max_content_length: '100mb', - }, - }, - }), - }), - }, - }, }; - expect(async () => validateMaxContentLength(server, logger.warning)).not.to.throwError(); + expect( + async () => await validateMaxContentLength(server, elasticsearch, logger.warning) + ).not.toThrow(); sinon.assert.notCalled(logger.warning); }); }); diff --git a/x-pack/legacy/plugins/reporting/server/lib/validate/validate_max_content_length.ts b/x-pack/legacy/plugins/reporting/server/lib/validate/validate_max_content_length.ts index f91cd40bfd3c73..ce4a5b93e74310 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/validate/validate_max_content_length.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/validate/validate_max_content_length.ts @@ -3,18 +3,24 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + import numeral from '@elastic/numeral'; +import { ElasticsearchServiceSetup } from 'kibana/server'; import { defaults, get } from 'lodash'; import { Logger, ServerFacade } from '../../../types'; const KIBANA_MAX_SIZE_BYTES_PATH = 'xpack.reporting.csv.maxSizeBytes'; const ES_MAX_SIZE_BYTES_PATH = 'http.max_content_length'; -export async function validateMaxContentLength(server: ServerFacade, logger: Logger) { +export async function validateMaxContentLength( + server: ServerFacade, + elasticsearch: ElasticsearchServiceSetup, + logger: Logger +) { const config = server.config(); - const { callWithInternalUser } = server.plugins.elasticsearch.getCluster('data'); + const { callAsInternalUser } = elasticsearch.dataClient; - const elasticClusterSettingsResponse = await callWithInternalUser('cluster.getSettings', { + const elasticClusterSettingsResponse = await callAsInternalUser('cluster.getSettings', { includeDefaults: true, }); const { persistent, transient, defaults: defaultSettings } = elasticClusterSettingsResponse; diff --git a/x-pack/legacy/plugins/reporting/server/plugin.ts b/x-pack/legacy/plugins/reporting/server/plugin.ts index cf66ec74969cae..a2938d442f7df0 100644 --- a/x-pack/legacy/plugins/reporting/server/plugin.ts +++ b/x-pack/legacy/plugins/reporting/server/plugin.ts @@ -5,20 +5,26 @@ */ import { Legacy } from 'kibana'; -import { CoreSetup, CoreStart, Plugin, LoggerFactory } from 'src/core/server'; +import { + CoreSetup, + CoreStart, + ElasticsearchServiceSetup, + LoggerFactory, + Plugin, +} from 'src/core/server'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { PluginStart as DataPluginStart } from '../../../../../src/plugins/data/server'; import { PluginSetupContract as SecurityPluginSetup } from '../../../../plugins/security/server'; -import { XPackMainPlugin } from '../../xpack_main/server/xpack_main'; // @ts-ignore import { mirrorPluginStatus } from '../../../server/lib/mirror_plugin_status'; +import { XPackMainPlugin } from '../../xpack_main/server/xpack_main'; import { PLUGIN_ID } from '../common/constants'; +import { logConfiguration } from '../log_configuration'; import { ReportingPluginSpecOptions } from '../types.d'; -import { registerRoutes } from './routes'; -import { checkLicenseFactory, getExportTypesRegistry, runValidations, LevelLogger } from './lib'; import { createBrowserDriverFactory } from './browsers'; +import { checkLicenseFactory, getExportTypesRegistry, LevelLogger, runValidations } from './lib'; +import { registerRoutes } from './routes'; import { registerReportingUsageCollector } from './usage'; -import { logConfiguration } from '../log_configuration'; -import { PluginStart as DataPluginStart } from '../../../../../src/plugins/data/server'; export interface ReportingInitializerContext { logger: LoggerFactory; @@ -29,22 +35,16 @@ export type ReportingSetup = object; export type ReportingStart = object; export interface ReportingSetupDeps { + elasticsearch: ElasticsearchServiceSetup; usageCollection: UsageCollectionSetup; security: SecurityPluginSetup; } export type ReportingStartDeps = object; -type LegacyPlugins = Legacy.Server['plugins']; - export interface LegacySetup { config: Legacy.Server['config']; info: Legacy.Server['info']; - plugins: { - elasticsearch: LegacyPlugins['elasticsearch']; - xpack_main: XPackMainPlugin & { - status?: any; - }; - }; + plugins: { xpack_main: XPackMainPlugin & { status?: any } }; route: Legacy.Server['route']; savedObjects: Legacy.Server['savedObjects']; uiSettingsServiceFactory: Legacy.Server['uiSettingsServiceFactory']; @@ -76,10 +76,10 @@ export function reportingPluginFactory( public async setup(core: CoreSetup, plugins: ReportingSetupDeps): Promise { const exportTypesRegistry = getExportTypesRegistry(); + const { usageCollection, elasticsearch } = plugins; let isCollectorReady = false; // Register a function with server to manage the collection of usage stats - const { usageCollection } = plugins; registerReportingUsageCollector( usageCollection, __LEGACY, @@ -91,7 +91,7 @@ export function reportingPluginFactory( const browserDriverFactory = await createBrowserDriverFactory(__LEGACY, logger); logConfiguration(__LEGACY, logger); - runValidations(__LEGACY, logger, browserDriverFactory); + runValidations(__LEGACY, elasticsearch, logger, browserDriverFactory); const { xpack_main: xpackMainPlugin } = __LEGACY.plugins; mirrorPluginStatus(xpackMainPlugin, legacyPlugin); diff --git a/x-pack/legacy/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts b/x-pack/legacy/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts index f3ed760bba4302..fd1d85fef0f21b 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts @@ -36,6 +36,7 @@ export function registerGenerateCsvFromSavedObjectImmediate( parentLogger: Logger ) { const routeOptions = getRouteOptionsCsv(server, plugins, parentLogger); + const { elasticsearch } = plugins; /* * CSV export with the `immediate` option does not queue a job with Reporting's ESQueue to run the job async. Instead, this does: @@ -57,8 +58,8 @@ export function registerGenerateCsvFromSavedObjectImmediate( * * Calling an execute job factory requires passing a browserDriverFactory option, so we should not call the factory from here */ - const createJobFn = createJobFactory(server, logger); - const executeJobFn = executeJobFactory(server, logger, { + const createJobFn = createJobFactory(server, elasticsearch, logger); + const executeJobFn = executeJobFactory(server, elasticsearch, logger, { browserDriverFactory: {} as HeadlessChromiumDriverFactory, }); const jobDocPayload: JobDocPayloadPanelCsv = await createJobFn( diff --git a/x-pack/legacy/plugins/reporting/server/routes/generation.ts b/x-pack/legacy/plugins/reporting/server/routes/generation.ts index 3c9ef6987b2d95..02a9541484bc63 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/generation.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/generation.ts @@ -5,6 +5,7 @@ */ import boom from 'boom'; +import { errors as elasticsearchErrors } from 'elasticsearch'; import { Legacy } from 'kibana'; import { API_BASE_URL } from '../../common/constants'; import { @@ -21,6 +22,8 @@ import { registerGenerateCsvFromSavedObject } from './generate_from_savedobject' import { registerGenerateCsvFromSavedObjectImmediate } from './generate_from_savedobject_immediate'; import { makeRequestFacade } from './lib/make_request_facade'; +const esErrors = elasticsearchErrors as Record; + export function registerJobGenerationRoutes( server: ServerFacade, plugins: ReportingSetupDeps, @@ -30,11 +33,15 @@ export function registerJobGenerationRoutes( ) { const config = server.config(); const DOWNLOAD_BASE_URL = config.get('server.basePath') + `${API_BASE_URL}/jobs/download`; - // @ts-ignore TODO - const { errors: esErrors } = server.plugins.elasticsearch.getCluster('admin'); - - const esqueue = createQueueFactory(server, logger, { exportTypesRegistry, browserDriverFactory }); - const enqueueJob = enqueueJobFactory(server, logger, { exportTypesRegistry, esqueue }); + const { elasticsearch } = plugins; + const esqueue = createQueueFactory(server, elasticsearch, logger, { + exportTypesRegistry, + browserDriverFactory, + }); + const enqueueJob = enqueueJobFactory(server, elasticsearch, logger, { + exportTypesRegistry, + esqueue, + }); /* * Generates enqueued job details to use in responses diff --git a/x-pack/legacy/plugins/reporting/server/routes/jobs.test.js b/x-pack/legacy/plugins/reporting/server/routes/jobs.test.js index c9d4f9fc027be3..811c81c502b812 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/jobs.test.js +++ b/x-pack/legacy/plugins/reporting/server/routes/jobs.test.js @@ -43,18 +43,12 @@ beforeEach(() => { jobContentEncoding: 'base64', jobContentExtension: 'pdf', }); - mockServer.plugins = { - elasticsearch: { - getCluster: memoize(() => ({ callWithInternalUser: jest.fn() })), - createCluster: () => ({ - callWithRequest: jest.fn(), - callWithInternalUser: jest.fn(), - }), - }, - }; }); const mockPlugins = { + elasticsearch: { + adminClient: { callAsInternalUser: jest.fn() }, + }, security: null, }; @@ -67,9 +61,9 @@ const getHits = (...sources) => { }; test(`returns 404 if job not found`, async () => { - mockServer.plugins.elasticsearch - .getCluster('admin') - .callWithInternalUser.mockReturnValue(Promise.resolve(getHits())); + mockPlugins.elasticsearch.adminClient = { + callAsInternalUser: jest.fn().mockReturnValue(Promise.resolve(getHits())), + }; registerJobInfoRoutes(mockServer, mockPlugins, exportTypesRegistry, mockLogger); @@ -84,9 +78,11 @@ test(`returns 404 if job not found`, async () => { }); test(`returns 401 if not valid job type`, async () => { - mockServer.plugins.elasticsearch - .getCluster('admin') - .callWithInternalUser.mockReturnValue(Promise.resolve(getHits({ jobtype: 'invalidJobType' }))); + mockPlugins.elasticsearch.adminClient = { + callAsInternalUser: jest + .fn() + .mockReturnValue(Promise.resolve(getHits({ jobtype: 'invalidJobType' }))), + }; registerJobInfoRoutes(mockServer, mockPlugins, exportTypesRegistry, mockLogger); @@ -101,11 +97,13 @@ test(`returns 401 if not valid job type`, async () => { describe(`when job is incomplete`, () => { const getIncompleteResponse = async () => { - mockServer.plugins.elasticsearch - .getCluster('admin') - .callWithInternalUser.mockReturnValue( - Promise.resolve(getHits({ jobtype: 'unencodedJobType', status: 'pending' })) - ); + mockPlugins.elasticsearch.adminClient = { + callAsInternalUser: jest + .fn() + .mockReturnValue( + Promise.resolve(getHits({ jobtype: 'unencodedJobType', status: 'pending' })) + ), + }; registerJobInfoRoutes(mockServer, mockPlugins, exportTypesRegistry, mockLogger); @@ -145,9 +143,9 @@ describe(`when job is failed`, () => { status: 'failed', output: { content: 'job failure message' }, }); - mockServer.plugins.elasticsearch - .getCluster('admin') - .callWithInternalUser.mockReturnValue(Promise.resolve(hits)); + mockPlugins.elasticsearch.adminClient = { + callAsInternalUser: jest.fn().mockReturnValue(Promise.resolve(hits)), + }; registerJobInfoRoutes(mockServer, mockPlugins, exportTypesRegistry, mockLogger); @@ -190,9 +188,9 @@ describe(`when job is completed`, () => { title, }, }); - mockServer.plugins.elasticsearch - .getCluster('admin') - .callWithInternalUser.mockReturnValue(Promise.resolve(hits)); + mockPlugins.elasticsearch.adminClient = { + callAsInternalUser: jest.fn().mockReturnValue(Promise.resolve(hits)), + }; registerJobInfoRoutes(mockServer, mockPlugins, exportTypesRegistry, mockLogger); diff --git a/x-pack/legacy/plugins/reporting/server/routes/jobs.ts b/x-pack/legacy/plugins/reporting/server/routes/jobs.ts index f9b731db5a702b..daabc2cf22f4e2 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/jobs.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/jobs.ts @@ -38,7 +38,8 @@ export function registerJobInfoRoutes( exportTypesRegistry: ExportTypesRegistry, logger: Logger ) { - const jobsQuery = jobsQueryFactory(server); + const { elasticsearch } = plugins; + const jobsQuery = jobsQueryFactory(server, elasticsearch); const getRouteConfig = getRouteConfigFactoryManagementPre(server, plugins, logger); const getRouteConfigDownload = getRouteConfigFactoryDownloadPre(server, plugins, logger); @@ -137,7 +138,7 @@ export function registerJobInfoRoutes( }); // trigger a download of the output from a job - const jobResponseHandler = jobResponseHandlerFactory(server, exportTypesRegistry); + const jobResponseHandler = jobResponseHandlerFactory(server, elasticsearch, exportTypesRegistry); server.route({ path: `${MAIN_ENTRY}/download/{docId}`, method: 'GET', diff --git a/x-pack/legacy/plugins/reporting/server/routes/lib/job_response_handler.ts b/x-pack/legacy/plugins/reporting/server/routes/lib/job_response_handler.ts index 3ba7aa30eedcb7..62f0d0a72b389a 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/lib/job_response_handler.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/lib/job_response_handler.ts @@ -5,10 +5,11 @@ */ import Boom from 'boom'; +import { ElasticsearchServiceSetup } from 'kibana/server'; import { ResponseToolkit } from 'hapi'; -import { ServerFacade, ExportTypesRegistry } from '../../../types'; -import { jobsQueryFactory } from '../../lib/jobs_query'; import { WHITELISTED_JOB_CONTENT_TYPES } from '../../../common/constants'; +import { ExportTypesRegistry, ServerFacade } from '../../../types'; +import { jobsQueryFactory } from '../../lib/jobs_query'; import { getDocumentPayloadFactory } from './get_document_payload'; interface JobResponseHandlerParams { @@ -21,9 +22,10 @@ interface JobResponseHandlerOpts { export function jobResponseHandlerFactory( server: ServerFacade, + elasticsearch: ElasticsearchServiceSetup, exportTypesRegistry: ExportTypesRegistry ) { - const jobsQuery = jobsQueryFactory(server); + const jobsQuery = jobsQueryFactory(server, elasticsearch); const getDocumentPayload = getDocumentPayloadFactory(server, exportTypesRegistry); return function jobResponseHandler( diff --git a/x-pack/legacy/plugins/reporting/types.d.ts b/x-pack/legacy/plugins/reporting/types.d.ts index 9ba016d8b828d8..a4ff39b23747dd 100644 --- a/x-pack/legacy/plugins/reporting/types.d.ts +++ b/x-pack/legacy/plugins/reporting/types.d.ts @@ -4,15 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ResponseObject } from 'hapi'; import { EventEmitter } from 'events'; +import { ResponseObject } from 'hapi'; +import { ElasticsearchServiceSetup } from 'kibana/server'; import { Legacy } from 'kibana'; import { CallCluster } from '../../../../src/legacy/core_plugins/elasticsearch'; import { CancellationToken } from './common/cancellation_token'; -import { LevelLogger } from './server/lib/level_logger'; import { HeadlessChromiumDriverFactory } from './server/browsers/chromium/driver_factory'; import { BrowserType } from './server/browsers/types'; -import { LegacySetup } from './server/plugin'; +import { LevelLogger } from './server/lib/level_logger'; +import { LegacySetup, ReportingSetupDeps } from './server/plugin'; export type ReportingPlugin = object; // For Plugin contract @@ -276,10 +277,12 @@ export interface ESQueueInstance { export type CreateJobFactory = ( server: ServerFacade, + elasticsearch: ElasticsearchServiceSetup, logger: LevelLogger ) => CreateJobFnType; export type ExecuteJobFactory = ( server: ServerFacade, + elasticsearch: ElasticsearchServiceSetup, logger: LevelLogger, opts: { browserDriverFactory: HeadlessChromiumDriverFactory; @@ -302,10 +305,10 @@ export interface ExportTypeDefinition< validLicenses: string[]; } -export { ExportTypesRegistry } from './server/lib/export_types_registry'; +export { CancellationToken } from './common/cancellation_token'; export { HeadlessChromiumDriver } from './server/browsers/chromium/driver'; export { HeadlessChromiumDriverFactory } from './server/browsers/chromium/driver_factory'; -export { CancellationToken } from './common/cancellation_token'; +export { ExportTypesRegistry } from './server/lib/export_types_registry'; // Prefer to import this type using: `import { LevelLogger } from 'relative/path/server/lib';` export { LevelLogger as Logger }; From 8d54fa8363039a0ded5de96bb78c44b6b6bb2403 Mon Sep 17 00:00:00 2001 From: Catherine Liu Date: Fri, 31 Jan 2020 13:47:41 -0700 Subject: [PATCH 11/33] [Canvas] Migrate Shareable Routes to NP (#56053) * Migrated shareables routes to NP * Added tests --- x-pack/legacy/plugins/canvas/server/plugin.ts | 3 - .../plugins/canvas/server/routes/index.ts | 12 --- .../canvas/server/routes/shareables.ts | 72 ----------------- x-pack/plugins/canvas/server/routes/index.ts | 6 +- .../server/routes/shareables/download.test.ts | 55 +++++++++++++ .../server/routes/shareables/download.ts | 33 ++++++++ .../canvas/server/routes/shareables/index.ts | 14 ++++ .../shareables/mock_shareable_workpad.json | 0 .../shareables/rendered_workpad_schema.ts | 77 ++++++++++++++++++ .../server/routes/shareables/zip.test.ts | 78 +++++++++++++++++++ .../canvas/server/routes/shareables/zip.ts | 37 +++++++++ 11 files changed, 298 insertions(+), 89 deletions(-) delete mode 100644 x-pack/legacy/plugins/canvas/server/routes/index.ts delete mode 100644 x-pack/legacy/plugins/canvas/server/routes/shareables.ts create mode 100644 x-pack/plugins/canvas/server/routes/shareables/download.test.ts create mode 100644 x-pack/plugins/canvas/server/routes/shareables/download.ts create mode 100644 x-pack/plugins/canvas/server/routes/shareables/index.ts create mode 100644 x-pack/plugins/canvas/server/routes/shareables/mock_shareable_workpad.json create mode 100644 x-pack/plugins/canvas/server/routes/shareables/rendered_workpad_schema.ts create mode 100644 x-pack/plugins/canvas/server/routes/shareables/zip.test.ts create mode 100644 x-pack/plugins/canvas/server/routes/shareables/zip.ts diff --git a/x-pack/legacy/plugins/canvas/server/plugin.ts b/x-pack/legacy/plugins/canvas/server/plugin.ts index ac3edbabce930e..1f17e85bfd294f 100644 --- a/x-pack/legacy/plugins/canvas/server/plugin.ts +++ b/x-pack/legacy/plugins/canvas/server/plugin.ts @@ -5,14 +5,11 @@ */ import { CoreSetup, PluginsSetup } from './shim'; -import { routes } from './routes'; import { functions } from '../canvas_plugin_src/functions/server'; import { loadSampleData } from './sample_data'; export class Plugin { public setup(core: CoreSetup, plugins: PluginsSetup) { - routes(core); - plugins.interpreter.register({ serverFunctions: functions }); core.injectUiAppVars('canvas', async () => { diff --git a/x-pack/legacy/plugins/canvas/server/routes/index.ts b/x-pack/legacy/plugins/canvas/server/routes/index.ts deleted file mode 100644 index 6898a3c459e3d4..00000000000000 --- a/x-pack/legacy/plugins/canvas/server/routes/index.ts +++ /dev/null @@ -1,12 +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 { shareableWorkpads } from './shareables'; -import { CoreSetup } from '../shim'; - -export function routes(setup: CoreSetup): void { - shareableWorkpads(setup.http.route); -} diff --git a/x-pack/legacy/plugins/canvas/server/routes/shareables.ts b/x-pack/legacy/plugins/canvas/server/routes/shareables.ts deleted file mode 100644 index e8186ceceb47f2..00000000000000 --- a/x-pack/legacy/plugins/canvas/server/routes/shareables.ts +++ /dev/null @@ -1,72 +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 archiver from 'archiver'; - -import { - API_ROUTE_SHAREABLE_RUNTIME, - API_ROUTE_SHAREABLE_RUNTIME_DOWNLOAD, - API_ROUTE_SHAREABLE_ZIP, -} from '../../common/lib/constants'; - -import { - SHAREABLE_RUNTIME_FILE, - SHAREABLE_RUNTIME_NAME, - SHAREABLE_RUNTIME_SRC, -} from '../../shareable_runtime/constants'; - -import { CoreSetup } from '../shim'; - -export function shareableWorkpads(route: CoreSetup['http']['route']) { - // get runtime - route({ - method: 'GET', - path: API_ROUTE_SHAREABLE_RUNTIME, - - handler: { - file: { - path: SHAREABLE_RUNTIME_FILE, - // The option setting is not for typical use. We're using it here to avoid - // problems in Cloud environments. See elastic/kibana#47405. - confine: false, - }, - }, - }); - - // download runtime - route({ - method: 'GET', - path: API_ROUTE_SHAREABLE_RUNTIME_DOWNLOAD, - - handler(_request, handler) { - // The option setting is not for typical use. We're using it here to avoid - // problems in Cloud environments. See elastic/kibana#47405. - // @ts-ignore No type for inert Hapi handler - const file = handler.file(SHAREABLE_RUNTIME_FILE, { confine: false }); - file.type('application/octet-stream'); - return file; - }, - }); - - route({ - method: 'POST', - path: API_ROUTE_SHAREABLE_ZIP, - handler(request, handler) { - const workpad = request.payload; - - const archive = archiver('zip'); - archive.append(JSON.stringify(workpad), { name: 'workpad.json' }); - archive.file(`${SHAREABLE_RUNTIME_SRC}/template.html`, { name: 'index.html' }); - archive.file(SHAREABLE_RUNTIME_FILE, { name: `${SHAREABLE_RUNTIME_NAME}.js` }); - - const response = handler.response(archive); - response.header('content-type', 'application/zip'); - archive.finalize(); - - return response; - }, - }); -} diff --git a/x-pack/plugins/canvas/server/routes/index.ts b/x-pack/plugins/canvas/server/routes/index.ts index e9afab5680332d..fce278e94bf32f 100644 --- a/x-pack/plugins/canvas/server/routes/index.ts +++ b/x-pack/plugins/canvas/server/routes/index.ts @@ -5,9 +5,10 @@ */ import { IRouter, Logger } from 'src/core/server'; -import { initWorkpadRoutes } from './workpad'; import { initCustomElementsRoutes } from './custom_elements'; import { initESFieldsRoutes } from './es_fields'; +import { initShareablesRoutes } from './shareables'; +import { initWorkpadRoutes } from './workpad'; export interface RouteInitializerDeps { router: IRouter; @@ -15,7 +16,8 @@ export interface RouteInitializerDeps { } export function initRoutes(deps: RouteInitializerDeps) { - initWorkpadRoutes(deps); initCustomElementsRoutes(deps); initESFieldsRoutes(deps); + initShareablesRoutes(deps); + initWorkpadRoutes(deps); } diff --git a/x-pack/plugins/canvas/server/routes/shareables/download.test.ts b/x-pack/plugins/canvas/server/routes/shareables/download.test.ts new file mode 100644 index 00000000000000..be4765217d7aa8 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/shareables/download.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. + */ + +jest.mock('fs'); + +import fs from 'fs'; +import { + IRouter, + kibanaResponseFactory, + RequestHandlerContext, + RequestHandler, +} from 'src/core/server'; +import { httpServiceMock, httpServerMock, loggingServiceMock } from 'src/core/server/mocks'; +import { initializeDownloadShareableWorkpadRoute } from './download'; + +const mockRouteContext = {} as RequestHandlerContext; +const path = `api/canvas/workpad/find`; +const mockRuntime = 'Canvas shareable runtime'; + +describe('Download Canvas shareables runtime', () => { + let routeHandler: RequestHandler; + + beforeEach(() => { + const httpService = httpServiceMock.createSetupContract(); + const router = httpService.createRouter('') as jest.Mocked; + initializeDownloadShareableWorkpadRoute({ + router, + logger: loggingServiceMock.create().get(), + }); + + routeHandler = router.get.mock.calls[0][1]; + }); + + afterAll(() => { + jest.restoreAllMocks(); + }); + + it(`returns 200 with canvas shareables runtime`, async () => { + const request = httpServerMock.createKibanaRequest({ + method: 'get', + path, + }); + + const readFileSyncMock = fs.readFileSync as jest.Mock; + readFileSyncMock.mockReturnValueOnce(mockRuntime); + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(response.status).toBe(200); + expect(response.payload).toMatchInlineSnapshot(`"Canvas shareable runtime"`); + }); +}); diff --git a/x-pack/plugins/canvas/server/routes/shareables/download.ts b/x-pack/plugins/canvas/server/routes/shareables/download.ts new file mode 100644 index 00000000000000..08bec1e4881aeb --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/shareables/download.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { readFileSync } from 'fs'; +import { SHAREABLE_RUNTIME_FILE } from '../../../../../legacy/plugins/canvas/shareable_runtime/constants'; +import { RouteInitializerDeps } from '../'; +import { API_ROUTE_SHAREABLE_RUNTIME_DOWNLOAD } from '../../../../../legacy/plugins/canvas/common/lib/constants'; + +export function initializeDownloadShareableWorkpadRoute(deps: RouteInitializerDeps) { + const { router } = deps; + router.get( + { + path: API_ROUTE_SHAREABLE_RUNTIME_DOWNLOAD, + validate: false, + }, + async (_context, _request, response) => { + // TODO: check if this is still an issue on cloud after migrating to NP + // + // The option setting is not for typical use. We're using it here to avoid + // problems in Cloud environments. See elastic/kibana#47405. + // @ts-ignore No type for inert Hapi handler + // const file = handler.file(SHAREABLE_RUNTIME_FILE, { confine: false }); + const file = readFileSync(SHAREABLE_RUNTIME_FILE); + return response.ok({ + headers: { 'content-type': 'application/octet-stream' }, + body: file, + }); + } + ); +} diff --git a/x-pack/plugins/canvas/server/routes/shareables/index.ts b/x-pack/plugins/canvas/server/routes/shareables/index.ts new file mode 100644 index 00000000000000..0aabd8b955b211 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/shareables/index.ts @@ -0,0 +1,14 @@ +/* + * 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 { RouteInitializerDeps } from '../'; +import { initializeZipShareableWorkpadRoute } from './zip'; +import { initializeDownloadShareableWorkpadRoute } from './download'; + +export function initShareablesRoutes(deps: RouteInitializerDeps) { + initializeDownloadShareableWorkpadRoute(deps); + initializeZipShareableWorkpadRoute(deps); +} diff --git a/x-pack/plugins/canvas/server/routes/shareables/mock_shareable_workpad.json b/x-pack/plugins/canvas/server/routes/shareables/mock_shareable_workpad.json new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/x-pack/plugins/canvas/server/routes/shareables/rendered_workpad_schema.ts b/x-pack/plugins/canvas/server/routes/shareables/rendered_workpad_schema.ts new file mode 100644 index 00000000000000..792200354724e4 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/shareables/rendered_workpad_schema.ts @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; + +export const PositionSchema = schema.object({ + angle: schema.number(), + height: schema.number(), + left: schema.number(), + parent: schema.nullable(schema.string()), + top: schema.number(), + width: schema.number(), +}); + +export const ContainerStyleSchema = schema.object({ + type: schema.maybe(schema.string()), + border: schema.maybe(schema.string()), + borderRadius: schema.maybe(schema.string()), + padding: schema.maybe(schema.string()), + backgroundColor: schema.maybe(schema.string()), + backgroundImage: schema.maybe(schema.string()), + backgroundSize: schema.maybe(schema.string()), + backgroundRepeat: schema.maybe(schema.string()), + opacity: schema.maybe(schema.number()), + overflow: schema.maybe(schema.string()), +}); + +export const RenderableSchema = schema.object({ + error: schema.nullable(schema.string()), + state: schema.string(), + value: schema.object({ + as: schema.string(), + containerStyle: ContainerStyleSchema, + css: schema.maybe(schema.string()), + type: schema.string(), + value: schema.any(), + }), +}); + +export const RenderedWorkpadElementSchema = schema.object({ + expressionRenderable: RenderableSchema, + id: schema.string(), + position: PositionSchema, +}); + +export const RenderedWorkpadPageSchema = schema.object({ + id: schema.string(), + elements: schema.arrayOf(RenderedWorkpadElementSchema), + groups: schema.maybe(schema.arrayOf(schema.arrayOf(RenderedWorkpadElementSchema))), + style: schema.recordOf(schema.string(), schema.string()), + transition: schema.maybe( + schema.oneOf([ + schema.object({}), + schema.object({ + name: schema.string(), + }), + ]) + ), +}); + +export const RenderedWorkpadSchema = schema.object({ + '@created': schema.maybe(schema.string()), + '@timestamp': schema.maybe(schema.string()), + assets: schema.maybe(schema.recordOf(schema.string(), RenderedWorkpadPageSchema)), + colors: schema.arrayOf(schema.string()), + css: schema.string(), + height: schema.number(), + id: schema.string(), + isWriteable: schema.maybe(schema.boolean()), + name: schema.string(), + page: schema.number(), + pages: schema.arrayOf(RenderedWorkpadPageSchema), + width: schema.number(), +}); diff --git a/x-pack/plugins/canvas/server/routes/shareables/zip.test.ts b/x-pack/plugins/canvas/server/routes/shareables/zip.test.ts new file mode 100644 index 00000000000000..edb59694a74005 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/shareables/zip.test.ts @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +jest.mock('archiver'); + +const archiver = require('archiver') as jest.Mock; +import { + IRouter, + kibanaResponseFactory, + RequestHandlerContext, + RequestHandler, +} from 'src/core/server'; +import { httpServiceMock, httpServerMock, loggingServiceMock } from 'src/core/server/mocks'; +import { initializeZipShareableWorkpadRoute } from './zip'; +import { API_ROUTE_SHAREABLE_ZIP } from '../../../../../legacy/plugins/canvas/common/lib'; +import { + SHAREABLE_RUNTIME_FILE, + SHAREABLE_RUNTIME_SRC, + SHAREABLE_RUNTIME_NAME, +} from '../../../../../legacy/plugins/canvas/shareable_runtime/constants'; + +const mockRouteContext = {} as RequestHandlerContext; +const mockWorkpad = {}; +const routePath = API_ROUTE_SHAREABLE_ZIP; + +describe('Zips Canvas shareables runtime together with workpad', () => { + let routeHandler: RequestHandler; + + beforeEach(() => { + const httpService = httpServiceMock.createSetupContract(); + const router = httpService.createRouter('') as jest.Mocked; + initializeZipShareableWorkpadRoute({ + router, + logger: loggingServiceMock.create().get(), + }); + + routeHandler = router.post.mock.calls[0][1]; + }); + + afterAll(() => { + jest.restoreAllMocks(); + }); + + it(`returns 200 with zip file with runtime and workpad`, async () => { + const request = httpServerMock.createKibanaRequest({ + method: 'get', + path: routePath, + body: mockWorkpad, + }); + + const mockArchive = { + append: jest.fn(), + file: jest.fn(), + finalize: jest.fn(), + }; + + archiver.mockReturnValueOnce(mockArchive); + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(response.status).toBe(200); + expect(response.payload).toBe(mockArchive); + expect(mockArchive.append).toHaveBeenCalledWith(JSON.stringify(mockWorkpad), { + name: 'workpad.json', + }); + expect(mockArchive.file).toHaveBeenCalledTimes(2); + expect(mockArchive.file).nthCalledWith(1, `${SHAREABLE_RUNTIME_SRC}/template.html`, { + name: 'index.html', + }); + expect(mockArchive.file).nthCalledWith(2, SHAREABLE_RUNTIME_FILE, { + name: `${SHAREABLE_RUNTIME_NAME}.js`, + }); + expect(mockArchive.finalize).toBeCalled(); + }); +}); diff --git a/x-pack/plugins/canvas/server/routes/shareables/zip.ts b/x-pack/plugins/canvas/server/routes/shareables/zip.ts new file mode 100644 index 00000000000000..e25b96cce96ff3 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/shareables/zip.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import archiver from 'archiver'; +import { API_ROUTE_SHAREABLE_ZIP } from '../../../../../legacy/plugins/canvas/common/lib'; +import { + SHAREABLE_RUNTIME_FILE, + SHAREABLE_RUNTIME_NAME, + SHAREABLE_RUNTIME_SRC, +} from '../../../../../legacy/plugins/canvas/shareable_runtime/constants'; +import { RenderedWorkpadSchema } from './rendered_workpad_schema'; +import { RouteInitializerDeps } from '..'; + +export function initializeZipShareableWorkpadRoute(deps: RouteInitializerDeps) { + const { router } = deps; + router.post( + { + path: API_ROUTE_SHAREABLE_ZIP, + validate: { body: RenderedWorkpadSchema }, + }, + async (_context, request, response) => { + const workpad = request.body; + const archive = archiver('zip'); + archive.append(JSON.stringify(workpad), { name: 'workpad.json' }); + archive.file(`${SHAREABLE_RUNTIME_SRC}/template.html`, { name: 'index.html' }); + archive.file(SHAREABLE_RUNTIME_FILE, { name: `${SHAREABLE_RUNTIME_NAME}.js` }); + + const result = { headers: { 'content-type': 'application/zip' }, body: archive }; + archive.finalize(); + + return response.ok(result); + } + ); +} From c72e8e0986c97dcd6e837537300ecc8b08a0aba5 Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Fri, 31 Jan 2020 16:32:20 -0500 Subject: [PATCH 12/33] [SIEM] [Detections] Import rules (#56389) * refactor import rules to do it by batch * fix error msg on api to match with json contract * review I * fix export --- .../__snapshots__/index.test.tsx.snap | 2 +- .../components/import_rule_modal/index.tsx | 7 +- .../routes/rules/export_rules_route.ts | 38 +- .../routes/rules/import_rules_route.ts | 337 ++++++++++-------- .../lib/detection_engine/routes/utils.ts | 10 + .../rules/get_export_by_object_ids.test.ts | 3 + .../rules/get_export_by_object_ids.ts | 75 ++-- 7 files changed, 288 insertions(+), 184 deletions(-) diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/__snapshots__/index.test.tsx.snap index 64b88912e53a3d..6b5ea2c5390f10 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/__snapshots__/index.test.tsx.snap @@ -47,7 +47,7 @@ exports[`ImportRuleModal renders correctly against snapshot 1`] = ` Cancel diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/index.tsx index 91b2ee283609fa..9a68797aea79b2 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/import_rule_modal/index.tsx @@ -90,6 +90,11 @@ export const ImportRuleModalComponent = ({ } }, [selectedFiles, overwrite]); + const handleCloseModal = useCallback(() => { + setSelectedFiles(null); + closeModal(); + }, [closeModal]); + return ( <> {showModal && ( @@ -125,7 +130,7 @@ export const ImportRuleModalComponent = ({ - {i18n.CANCEL_BUTTON} + {i18n.CANCEL_BUTTON} ('savedObjects.maxImportExportSize'); - if (request.payload?.objects != null && request.payload.objects.length > exportSizeLimit) { - return Boom.badRequest(`Can't export more than ${exportSizeLimit} rules`); - } else { - const nonPackagedRulesCount = await getNonPackagedRulesCount({ alertsClient }); - if (nonPackagedRulesCount > exportSizeLimit) { + try { + const exportSizeLimit = server.config().get('savedObjects.maxImportExportSize'); + if (request.payload?.objects != null && request.payload.objects.length > exportSizeLimit) { return Boom.badRequest(`Can't export more than ${exportSizeLimit} rules`); + } else { + const nonPackagedRulesCount = await getNonPackagedRulesCount({ alertsClient }); + if (nonPackagedRulesCount > exportSizeLimit) { + return Boom.badRequest(`Can't export more than ${exportSizeLimit} rules`); + } } - } - const exported = - request.payload?.objects != null - ? await getExportByObjectIds(alertsClient, request.payload.objects) - : await getExportAll(alertsClient); + const exported = + request.payload?.objects != null + ? await getExportByObjectIds(alertsClient, request.payload.objects) + : await getExportAll(alertsClient); - const response = request.query.exclude_export_details - ? headers.response(exported.rulesNdjson) - : headers.response(`${exported.rulesNdjson}${exported.exportDetails}`); + const response = request.query.exclude_export_details + ? headers.response(exported.rulesNdjson) + : headers.response(`${exported.rulesNdjson}${exported.exportDetails}`); - return response - .header('Content-Disposition', `attachment; filename="${request.query.file_name}"`) - .header('Content-Type', 'application/ndjson'); + return response + .header('Content-Disposition', `attachment; filename="${request.query.file_name}"`) + .header('Content-Type', 'application/ndjson'); + } catch { + return Boom.badRequest(`Sorry, something went wrong to export rules`); + } }, }; }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts index 71fdef3623bc71..0d57f5739fc15a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts @@ -6,8 +6,9 @@ import Boom from 'boom'; import Hapi from 'hapi'; +import { chunk, isEmpty, isFunction } from 'lodash/fp'; import { extname } from 'path'; -import { isFunction } from 'lodash/fp'; +import uuid from 'uuid'; import { createPromiseFromStreams } from '../../../../../../../../../src/legacy/utils/streams'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { createRules } from '../../rules/create_rules'; @@ -18,17 +19,25 @@ import { getIndexExists } from '../../index/get_index_exists'; import { callWithRequestFactory, getIndex, - createImportErrorObject, - transformImportError, - ImportSuccessError, + createBulkErrorObject, + ImportRuleResponse, } from '../utils'; import { createRulesStreamFromNdJson } from '../../rules/create_rules_stream_from_ndjson'; import { ImportRuleAlertRest } from '../../types'; -import { transformOrImportError } from './utils'; import { updateRules } from '../../rules/update_rules'; import { importRulesQuerySchema, importRulesPayloadSchema } from '../schemas/import_rules_schema'; import { KibanaRequest } from '../../../../../../../../../src/core/server'; +type PromiseFromStreams = ImportRuleAlertRest | Error; + +/* + * We were getting some error like that possible EventEmitter memory leak detected + * So we decide to batch the update by 10 to avoid any complication in the node side + * https://nodejs.org/docs/latest/api/events.html#events_emitter_setmaxlisteners_n + * + */ +const CHUNK_PARSED_OBJECT_SIZE = 10; + export const createImportRulesRoute = (server: ServerFacade): Hapi.ServerRoute => { return { method: 'POST', @@ -67,145 +76,189 @@ export const createImportRulesRoute = (server: ServerFacade): Hapi.ServerRoute = const objectLimit = server.config().get('savedObjects.maxImportExportSize'); const readStream = createRulesStreamFromNdJson(request.payload.file, objectLimit); - const parsedObjects = await createPromiseFromStreams<[ImportRuleAlertRest | Error]>([ - readStream, - ]); - - const reduced = await parsedObjects.reduce>( - async (accum, parsedRule) => { - const existingImportSuccessError = await accum; - if (parsedRule instanceof Error) { - // If the JSON object had a validation or parse error then we return - // early with the error and an (unknown) for the ruleId - return createImportErrorObject({ - ruleId: '(unknown)', // TODO: Better handling where we know which ruleId is having issues with imports - statusCode: 400, - message: parsedRule.message, - existingImportSuccessError, - }); - } + const parsedObjects = await createPromiseFromStreams([readStream]); - const { - description, - enabled, - false_positives: falsePositives, - from, - immutable, - query, - language, - output_index: outputIndex, - saved_id: savedId, - meta, - filters, - rule_id: ruleId, - index, - interval, - max_signals: maxSignals, - risk_score: riskScore, - name, - severity, - tags, - threat, - to, - type, - references, - timeline_id: timelineId, - timeline_title: timelineTitle, - version, - } = parsedRule; - try { - const finalIndex = outputIndex != null ? outputIndex : getIndex(request, server); - const callWithRequest = callWithRequestFactory(request, server); - const indexExists = await getIndexExists(callWithRequest, finalIndex); - if (!indexExists) { - return createImportErrorObject({ - ruleId, - statusCode: 409, - message: `To create a rule, the index must exist first. Index ${finalIndex} does not exist`, - existingImportSuccessError, - }); - } - const rule = await readRules({ alertsClient, ruleId }); - if (rule == null) { - const createdRule = await createRules({ - alertsClient, - actionsClient, - description, - enabled, - falsePositives, - from, - immutable, - query, - language, - outputIndex: finalIndex, - savedId, - timelineId, - timelineTitle, - meta, - filters, - ruleId, - index, - interval, - maxSignals, - riskScore, - name, - severity, - tags, - to, - type, - threat, - references, - version, - }); - return transformOrImportError(ruleId, createdRule, existingImportSuccessError); - } else if (rule != null && request.query.overwrite) { - const updatedRule = await updateRules({ - alertsClient, - actionsClient, - savedObjectsClient, - description, - enabled, - falsePositives, - from, - immutable, - query, - language, - outputIndex, - savedId, - timelineId, - timelineTitle, - meta, - filters, - id: undefined, - ruleId, - index, - interval, - maxSignals, - riskScore, - name, - severity, - tags, - to, - type, - threat, - references, - version, - }); - return transformOrImportError(ruleId, updatedRule, existingImportSuccessError); - } else { - return existingImportSuccessError; - } - } catch (err) { - return transformImportError(ruleId, err, existingImportSuccessError); - } - }, - Promise.resolve({ - success: true, - success_count: 0, - errors: [], - }) + const uniqueParsedObjects = Array.from( + parsedObjects + .reduce( + (acc, parsedRule) => { + if (parsedRule instanceof Error) { + acc.set(uuid.v4(), parsedRule); + } else { + const { rule_id: ruleId } = parsedRule; + if (ruleId != null) { + acc.set(ruleId, parsedRule); + } else { + acc.set(uuid.v4(), parsedRule); + } + } + return acc; + }, // using map (preserves ordering) + new Map() + ) + .values() ); - return reduced; + + const chunkParseObjects = chunk(CHUNK_PARSED_OBJECT_SIZE, uniqueParsedObjects); + let importRuleResponse: ImportRuleResponse[] = []; + + while (chunkParseObjects.length) { + const batchParseObjects = chunkParseObjects.shift() ?? []; + const newImportRuleResponse = await Promise.all( + batchParseObjects.reduce>>((accum, parsedRule) => { + const importsWorkerPromise = new Promise( + async (resolve, reject) => { + if (parsedRule instanceof Error) { + // If the JSON object had a validation or parse error then we return + // early with the error and an (unknown) for the ruleId + resolve( + createBulkErrorObject({ + ruleId: '(unknown)', + statusCode: 400, + message: parsedRule.message, + }) + ); + return null; + } + const { + description, + false_positives: falsePositives, + from, + immutable, + query, + language, + output_index: outputIndex, + saved_id: savedId, + meta, + filters, + rule_id: ruleId, + index, + interval, + max_signals: maxSignals, + risk_score: riskScore, + name, + severity, + tags, + threat, + to, + type, + references, + timeline_id: timelineId, + timeline_title: timelineTitle, + version, + } = parsedRule; + try { + const finalIndex = getIndex(request, server); + const callWithRequest = callWithRequestFactory(request, server); + const indexExists = await getIndexExists(callWithRequest, finalIndex); + if (!indexExists) { + resolve( + createBulkErrorObject({ + ruleId, + statusCode: 409, + message: `To create a rule, the index must exist first. Index ${finalIndex} does not exist`, + }) + ); + } + const rule = await readRules({ alertsClient, ruleId }); + if (rule == null) { + await createRules({ + alertsClient, + actionsClient, + description, + enabled: false, + falsePositives, + from, + immutable, + query, + language, + outputIndex: finalIndex, + savedId, + timelineId, + timelineTitle, + meta, + filters, + ruleId, + index, + interval, + maxSignals, + riskScore, + name, + severity, + tags, + to, + type, + threat, + references, + version, + }); + resolve({ rule_id: ruleId, status_code: 200 }); + } else if (rule != null && request.query.overwrite) { + await updateRules({ + alertsClient, + actionsClient, + savedObjectsClient, + description, + enabled: false, + falsePositives, + from, + immutable, + query, + language, + outputIndex, + savedId, + timelineId, + timelineTitle, + meta, + filters, + id: undefined, + ruleId, + index, + interval, + maxSignals, + riskScore, + name, + severity, + tags, + to, + type, + threat, + references, + version, + }); + resolve({ rule_id: ruleId, status_code: 200 }); + } else if (rule != null) { + resolve( + createBulkErrorObject({ + ruleId, + statusCode: 409, + message: `This Rule "${rule.name}" already exists`, + }) + ); + } + } catch (err) { + resolve( + createBulkErrorObject({ + ruleId, + statusCode: 400, + message: err.message, + }) + ); + } + } + ); + return [...accum, importsWorkerPromise]; + }, []) + ); + importRuleResponse = [...importRuleResponse, ...newImportRuleResponse]; + } + + const errorsResp = importRuleResponse.filter(resp => !isEmpty(resp.error)); + return { + success: errorsResp.length === 0, + success_count: importRuleResponse.filter(resp => resp.status_code === 200).length, + errors: errorsResp, + }; }, }; }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts index 19cd972b60e1a8..416c76b5d4eb5d 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts @@ -52,6 +52,16 @@ export const createBulkErrorObject = ({ }; }; +export interface ImportRuleResponse { + rule_id: string; + status_code?: number; + message?: string; + error?: { + status_code: number; + message: string; + }; +} + export interface ImportSuccessError { success: boolean; success_count: number; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts index 05e455efb3f22e..236d04acc782b8 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts @@ -66,6 +66,7 @@ describe('get_export_by_object_ids', () => { const objects = [{ rule_id: 'rule-1' }]; const exports = await getRulesFromObjects(unsafeCast, objects); const expected: RulesErrors = { + exportedCount: 1, missingRules: [], rules: [ { @@ -141,6 +142,7 @@ describe('get_export_by_object_ids', () => { const objects = [{ rule_id: 'rule-1' }]; const exports = await getRulesFromObjects(unsafeCast, objects); const expected: RulesErrors = { + exportedCount: 0, missingRules: [{ rule_id: 'rule-1' }], rules: [], }; @@ -164,6 +166,7 @@ describe('get_export_by_object_ids', () => { const objects = [{ rule_id: 'rule-1' }]; const exports = await getRulesFromObjects(unsafeCast, objects); const expected: RulesErrors = { + exportedCount: 0, missingRules: [{ rule_id: 'rule-1' }], rules: [], }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.ts index a5cf1bbfb78588..7e0d61d040617e 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.ts @@ -11,7 +11,20 @@ import { readRules } from './read_rules'; import { transformRulesToNdjson, transformAlertToRule } from '../routes/rules/utils'; import { OutputRuleAlertRest } from '../types'; +interface ExportSuccesRule { + statusCode: 200; + rule: Partial; +} + +interface ExportFailedRule { + statusCode: 404; + missingRuleId: { rule_id: string }; +} + +type ExportRules = ExportSuccesRule | ExportFailedRule; + export interface RulesErrors { + exportedCount: number; missingRules: Array<{ rule_id: string }>; rules: Array>; } @@ -33,28 +46,44 @@ export const getRulesFromObjects = async ( alertsClient: AlertsClient, objects: Array<{ rule_id: string }> ): Promise => { - const alertsAndErrors = await objects.reduce>( - async (accumPromise, object) => { - const accum = await accumPromise; - const rule = await readRules({ alertsClient, ruleId: object.rule_id }); - if (rule != null && isAlertType(rule) && rule.params.immutable !== true) { - const transformedRule = transformAlertToRule(rule); - return { - missingRules: accum.missingRules, - rules: [...accum.rules, transformedRule], - }; - } else { - return { - missingRules: [...accum.missingRules, { rule_id: object.rule_id }], - rules: accum.rules, - }; - } - }, - Promise.resolve({ - exportedCount: 0, - missingRules: [], - rules: [], - }) + const alertsAndErrors = await Promise.all( + objects.reduce>>((accumPromise, object) => { + const exportWorkerPromise = new Promise(async resolve => { + try { + const rule = await readRules({ alertsClient, ruleId: object.rule_id }); + if (rule != null && isAlertType(rule) && rule.params.immutable !== true) { + const transformedRule = transformAlertToRule(rule); + resolve({ + statusCode: 200, + rule: transformedRule, + }); + } else { + resolve({ + statusCode: 404, + missingRuleId: { rule_id: object.rule_id }, + }); + } + } catch { + resolve({ + statusCode: 404, + missingRuleId: { rule_id: object.rule_id }, + }); + } + }); + return [...accumPromise, exportWorkerPromise]; + }, []) ); - return alertsAndErrors; + + const missingRules = alertsAndErrors.filter( + resp => resp.statusCode === 404 + ) as ExportFailedRule[]; + const exportedRules = alertsAndErrors.filter( + resp => resp.statusCode === 200 + ) as ExportSuccesRule[]; + + return { + exportedCount: exportedRules.length, + missingRules: missingRules.map(mr => mr.missingRuleId), + rules: exportedRules.map(er => er.rule), + }; }; From 3601602feb462eb6a6f1fade7ca41370cc18952b Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Fri, 31 Jan 2020 16:32:30 -0500 Subject: [PATCH 13/33] fix update severity (#56527) --- .../rules/components/step_about_rule/index.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.tsx index 4ae88b3014d190..9c351e66c2f04a 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.tsx @@ -168,7 +168,6 @@ const StepAboutRuleComponent: FC = ({ min: 0, fullWidth: false, disabled: isLoading, - options: severityOptions, showTicks: true, tickInterval: 25, }, @@ -239,8 +238,13 @@ const StepAboutRuleComponent: FC = ({ {({ severity }) => { const newRiskScore = defaultRiskScoreBySeverity[severity as SeverityValue]; + const severityField = form.getFields().severity; const riskScoreField = form.getFields().riskScore; - if (newRiskScore != null && riskScoreField.value !== newRiskScore) { + if ( + severityField.value !== severity && + newRiskScore != null && + riskScoreField.value !== newRiskScore + ) { riskScoreField.setValue(newRiskScore); } return null; From 3c2c689e981dbafe0ad8f6af78392ed88282be46 Mon Sep 17 00:00:00 2001 From: Alex Holmansky Date: Fri, 31 Jan 2020 17:19:16 -0500 Subject: [PATCH 14/33] Use diagnostics-enable action in the workflow. Issue: #56526 (#56530) --- .github/workflows/pr-project-assigner.yml | 2 +- .github/workflows/project-assigner.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr-project-assigner.yml b/.github/workflows/pr-project-assigner.yml index 708c9efea404ba..ccaa5b11aa80c1 100644 --- a/.github/workflows/pr-project-assigner.yml +++ b/.github/workflows/pr-project-assigner.yml @@ -8,7 +8,7 @@ jobs: name: Assign a PR to project based on label steps: - name: Assign to project - uses: elastic/github-actions/project-assigner@v1.0.0 + uses: elastic/github-actions/project-assigner@v1.0.1 id: project_assigner with: issue-mappings: | diff --git a/.github/workflows/project-assigner.yml b/.github/workflows/project-assigner.yml index aec3bf88f0ee20..737da4f7fe3710 100644 --- a/.github/workflows/project-assigner.yml +++ b/.github/workflows/project-assigner.yml @@ -8,7 +8,7 @@ jobs: name: Assign issue or PR to project based on label steps: - name: Assign to project - uses: elastic/github-actions/project-assigner@v1.0.0 + uses: elastic/github-actions/project-assigner@v1.0.1 id: project_assigner with: issue-mappings: '[{"label": "Team:AppArch", "projectName": "kibana-app-arch", "columnId": 6173895}, {"label": "Feature:Lens", "projectName": "Lens", "columnId": 6219363}, {"label": "Team:Canvas", "projectName": "canvas", "columnId": 6187593}]' From 1c705d82486ffb9238bd008dc22e770059e124c0 Mon Sep 17 00:00:00 2001 From: Nathan L Smith Date: Fri, 31 Jan 2020 17:06:06 -0600 Subject: [PATCH 15/33] APM Service Map popover fixes round 1 (#56502) Some fixes for #54405. * Use the `popoverPositionFluid` method of `EuiPopover` to properly position the arrow based on where the popover was opened * Rearrange the variable declarations and effects in the popover index * Create a `Contents` component to separate the internals of the popover from the index, where the poisitioning is done * Add a story to show the contents rendering --- .../app/ServiceMap/Popover/Contents.tsx | 66 ++++++++++ .../app/ServiceMap/Popover/Info.tsx | 9 +- .../ServiceMap/Popover/Popover.stories.tsx | 49 +++++++ .../app/ServiceMap/Popover/index.tsx | 124 +++++++----------- 4 files changed, 171 insertions(+), 77 deletions(-) create mode 100644 x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx create mode 100644 x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx new file mode 100644 index 00000000000000..378ad9509c2170 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiTitle +} from '@elastic/eui'; +import cytoscape from 'cytoscape'; +import React from 'react'; +import { Buttons } from './Buttons'; +import { Info } from './Info'; +import { ServiceMetricList } from './ServiceMetricList'; + +const popoverMinWidth = 280; + +interface ContentsProps { + focusedServiceName?: string; + isService: boolean; + label: string; + onFocusClick: () => void; + selectedNodeData: cytoscape.NodeDataDefinition; + selectedNodeServiceName: string; +} + +export function Contents({ + selectedNodeData, + focusedServiceName, + isService, + label, + onFocusClick, + selectedNodeServiceName +}: ContentsProps) { + return ( + + + +

{label}

+ + + + + {isService ? ( + + ) : ( + + )} + + {isService && ( + + )} + + ); +} diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Info.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Info.tsx index 1c5443e404f9b3..d432119505382a 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Info.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Info.tsx @@ -4,10 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import lightTheme from '@elastic/eui/dist/eui_theme_light.json'; import { i18n } from '@kbn/i18n'; +import cytoscape from 'cytoscape'; +import React from 'react'; import styled from 'styled-components'; -import lightTheme from '@elastic/eui/dist/eui_theme_light.json'; const ItemRow = styled.div` line-height: 2; @@ -19,8 +20,8 @@ const ItemTitle = styled.dt` const ItemDescription = styled.dd``; -interface InfoProps { - type: string; +interface InfoProps extends cytoscape.NodeDataDefinition { + type?: string; subtype?: string; } diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx new file mode 100644 index 00000000000000..b26488c5ef7de9 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { storiesOf } from '@storybook/react'; +import React from 'react'; +import { + ApmPluginContext, + ApmPluginContextValue +} from '../../../../context/ApmPluginContext'; +import { Contents } from './Contents'; + +const selectedNodeData = { + id: 'opbeans-node', + label: 'opbeans-node', + href: + '#/services/opbeans-node/service-map?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0', + agentName: 'nodejs', + type: 'service' +}; + +storiesOf('app/ServiceMap/Popover/Contents', module).add( + 'example', + () => { + return ( + + {}} + selectedNodeServiceName="opbeans-node" + /> + + ); + }, + { + info: { + propTablesExclude: [ApmPluginContext.Provider], + source: false + } + } +); diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/index.tsx index dfb78aaa0214c4..e8e37cfdfb1f0f 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/index.tsx @@ -4,27 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - EuiFlexGroup, - EuiFlexItem, - EuiHorizontalRule, - EuiPopover, - EuiTitle -} from '@elastic/eui'; +import { EuiPopover } from '@elastic/eui'; import cytoscape from 'cytoscape'; import React, { CSSProperties, + useCallback, useContext, useEffect, - useState, - useCallback + useRef, + useState } from 'react'; import { CytoscapeContext } from '../Cytoscape'; -import { Buttons } from './Buttons'; -import { Info } from './Info'; -import { ServiceMetricList } from './ServiceMetricList'; - -const popoverMinWidth = 280; +import { Contents } from './Contents'; interface PopoverProps { focusedServiceName?: string; @@ -35,56 +26,62 @@ export function Popover({ focusedServiceName }: PopoverProps) { const [selectedNode, setSelectedNode] = useState< cytoscape.NodeSingular | undefined >(undefined); - const onFocusClick = useCallback(() => setSelectedNode(undefined), [ + const deselect = useCallback(() => setSelectedNode(undefined), [ setSelectedNode ]); - - useEffect(() => { - const selectHandler: cytoscape.EventHandler = event => { - setSelectedNode(event.target); - }; - const unselectHandler: cytoscape.EventHandler = () => { - setSelectedNode(undefined); - }; - - if (cy) { - cy.on('select', 'node', selectHandler); - cy.on('unselect', 'node', unselectHandler); - cy.on('data viewport', unselectHandler); - } - - return () => { - if (cy) { - cy.removeListener('select', 'node', selectHandler); - cy.removeListener('unselect', 'node', unselectHandler); - cy.removeListener('data viewport', undefined, unselectHandler); - } - }; - }, [cy]); - const renderedHeight = selectedNode?.renderedHeight() ?? 0; const renderedWidth = selectedNode?.renderedWidth() ?? 0; const { x, y } = selectedNode?.renderedPosition() ?? { x: 0, y: 0 }; const isOpen = !!selectedNode; - const selectedNodeServiceName: string = selectedNode?.data('id'); const isService = selectedNode?.data('type') === 'service'; const triggerStyle: CSSProperties = { background: 'transparent', height: renderedHeight, position: 'absolute', - width: renderedWidth + width: renderedWidth, + border: '3px dotted red' }; - const trigger =
; - + const trigger =
; const zoom = cy?.zoom() ?? 1; const height = selectedNode?.height() ?? 0; - const translateY = y - (zoom + 1) * (height / 2); + const translateY = y - ((zoom + 1) * height) / 4; const popoverStyle: CSSProperties = { position: 'absolute', transform: `translate(${x}px, ${translateY}px)` }; - const data = selectedNode?.data() ?? {}; - const label = data.label || selectedNodeServiceName; + const selectedNodeData = selectedNode?.data() ?? {}; + const selectedNodeServiceName = selectedNodeData.id; + const label = selectedNodeData.label || selectedNodeServiceName; + const popoverRef = useRef(null); + + // Set up Cytoscape event handlers + useEffect(() => { + const selectHandler: cytoscape.EventHandler = event => { + setSelectedNode(event.target); + }; + + if (cy) { + cy.on('select', 'node', selectHandler); + cy.on('unselect', 'node', deselect); + cy.on('data viewport', deselect); + } + + return () => { + if (cy) { + cy.removeListener('select', 'node', selectHandler); + cy.removeListener('unselect', 'node', deselect); + cy.removeListener('data viewport', undefined, deselect); + } + }; + }, [cy, deselect]); + + // Handle positioning of popover. This makes it so the popover positions + // itself correctly and the arrows are always pointing to where they should. + useEffect(() => { + if (popoverRef.current) { + popoverRef.current.positionPopoverFluid(); + } + }, [popoverRef, x, y]); return ( {}} isOpen={isOpen} + ref={popoverRef} style={popoverStyle} > - - - -

{label}

-
- -
- - - {isService ? ( - - ) : ( - - )} - - {isService && ( - - )} -
+
); } From 49e9c793dfeb3ef545b6060df6037d86b945f99f Mon Sep 17 00:00:00 2001 From: Shahzad Date: Sat, 1 Feb 2020 00:20:29 +0100 Subject: [PATCH 16/33] [Uptime] Update relative time handling (#55693) * update relative time handling * fix type * update logic to handle end date * fix test * PR feedback * refactor code * refactor interval time * fix tests Co-authored-by: Elastic Machine --- .../url_params/get_supported_url_params.ts | 3 ++- .../helper/url_params/parse_absolute_date.ts | 4 +-- .../search/find_potential_matches.ts | 1 - .../monitor_states/search/query_context.ts | 7 ++--- .../__test__/parse_relative_date.test.ts | 23 ++++++++++++++++ .../lib/helper/get_histogram_interval.ts | 27 ++++++++++++++++--- 6 files changed, 55 insertions(+), 10 deletions(-) create mode 100644 x-pack/legacy/plugins/uptime/server/lib/helper/__test__/parse_relative_date.test.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/get_supported_url_params.ts b/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/get_supported_url_params.ts index c1382d455313f0..f01448d9e37acd 100644 --- a/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/get_supported_url_params.ts +++ b/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/get_supported_url_params.ts @@ -84,7 +84,8 @@ export const getSupportedUrlParams = (params: { ), absoluteDateRangeEnd: parseAbsoluteDate( dateRangeEnd || DATE_RANGE_END, - ABSOLUTE_DATE_RANGE_END + ABSOLUTE_DATE_RANGE_END, + { roundUp: true } ), autorefreshInterval: parseUrlInt(autorefreshInterval, AUTOREFRESH_INTERVAL), autorefreshIsPaused: parseIsPaused(autorefreshIsPaused, AUTOREFRESH_IS_PAUSED), diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/parse_absolute_date.ts b/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/parse_absolute_date.ts index eaf720a8d2f7e1..2b0921f07abc97 100644 --- a/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/parse_absolute_date.ts +++ b/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/parse_absolute_date.ts @@ -6,8 +6,8 @@ import DateMath from '@elastic/datemath'; -export const parseAbsoluteDate = (date: string, defaultValue: number): number => { - const momentWrapper = DateMath.parse(date); +export const parseAbsoluteDate = (date: string, defaultValue: number, options = {}): number => { + const momentWrapper = DateMath.parse(date, options); if (momentWrapper) { return momentWrapper.valueOf(); } diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/find_potential_matches.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/find_potential_matches.ts index e34bc6ab805c06..634d6369531d8c 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/find_potential_matches.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/find_potential_matches.ts @@ -25,7 +25,6 @@ export const findPotentialMatches = async ( size: number ) => { const queryResult = await query(queryContext, searchAfter, size); - const checkGroups = new Set(); const monitorIds: string[] = []; get(queryResult, 'aggregations.monitors.buckets', []).forEach((b: any) => { diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/query_context.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/query_context.ts index d97b7653402a32..961cc94dcea19f 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/query_context.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/query_context.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import DateMath from '@elastic/datemath'; import { APICaller } from 'kibana/server'; import { CursorPagination } from '../adapter_types'; import { INDEX_NAMES } from '../../../../../common/constants'; +import { parseRelativeDate } from '../../../helper/get_histogram_interval'; export class QueryContext { callES: APICaller; @@ -95,8 +95,9 @@ export class QueryContext { // latencies and slowdowns that's dangerous. Making this value larger makes things // only slower, but only marginally so, and prevents people from seeing weird // behavior. - const tsStart = DateMath.parse(this.dateRangeEnd)!.subtract(5, 'minutes'); - const tsEnd = DateMath.parse(this.dateRangeEnd)!; + + const tsEnd = parseRelativeDate(this.dateRangeEnd, { roundUp: true })!; + const tsStart = tsEnd.subtract(5, 'minutes'); return { range: { diff --git a/x-pack/legacy/plugins/uptime/server/lib/helper/__test__/parse_relative_date.test.ts b/x-pack/legacy/plugins/uptime/server/lib/helper/__test__/parse_relative_date.test.ts new file mode 100644 index 00000000000000..ec6e48c62c62e0 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/server/lib/helper/__test__/parse_relative_date.test.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { parseRelativeDate } from '../get_histogram_interval'; +import { Moment } from 'moment'; + +describe('Parsing a relative end date properly', () => { + it('converts the upper range of relative end dates to now', async () => { + const thisWeekEndDate = 'now/w'; + + let endDate = parseRelativeDate(thisWeekEndDate, { roundUp: true }); + expect(Date.now() - (endDate as Moment).valueOf()).toBeLessThan(1000); + + const todayEndDate = 'now/d'; + + endDate = parseRelativeDate(todayEndDate, { roundUp: true }); + + expect(Date.now() - (endDate as Moment).valueOf()).toBeLessThan(1000); + }); +}); diff --git a/x-pack/legacy/plugins/uptime/server/lib/helper/get_histogram_interval.ts b/x-pack/legacy/plugins/uptime/server/lib/helper/get_histogram_interval.ts index 0dedc3e456f513..26515fb4b4c63b 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/helper/get_histogram_interval.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/helper/get_histogram_interval.ts @@ -7,18 +7,39 @@ import DateMath from '@elastic/datemath'; import { QUERY } from '../../../common/constants'; +export const parseRelativeDate = (dateStr: string, options = {}) => { + // We need this this parsing because if user selects This week or this date + // That represents end date in future, if week or day is still in the middle + // Uptime data can never be collected in future, so we will reset date to now + // in That case. Example case we select this week range will be to='now/w' and from = 'now/w'; + + const parsedDate = DateMath.parse(dateStr, options); + const dateTimestamp = parsedDate?.valueOf() ?? 0; + if (dateTimestamp > Date.now()) { + return DateMath.parse('now'); + } + return parsedDate; +}; + export const getHistogramInterval = ( dateRangeStart: string, dateRangeEnd: string, bucketCount?: number ): number => { - const from = DateMath.parse(dateRangeStart); - const to = DateMath.parse(dateRangeEnd); + const from = parseRelativeDate(dateRangeStart); + + // roundUp is required for relative date like now/w to get the end of the week + const to = parseRelativeDate(dateRangeEnd, { roundUp: true }); if (from === undefined) { throw Error('Invalid dateRangeStart value'); } if (to === undefined) { throw Error('Invalid dateRangeEnd value'); } - return Math.round((to.valueOf() - from.valueOf()) / (bucketCount || QUERY.DEFAULT_BUCKET_COUNT)); + const interval = Math.round( + (to.valueOf() - from.valueOf()) / (bucketCount || QUERY.DEFAULT_BUCKET_COUNT) + ); + + // Interval can never be zero, if it's 0 we return at least 1ms interval + return interval > 0 ? interval : 1; }; From 0a17cde3fb375e24daf2aa13e8e51d5ae1a3ca83 Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Fri, 31 Jan 2020 18:34:59 -0500 Subject: [PATCH 17/33] [SIEM] [Detections] [BUG] Feedback to user about generated encryption key (#56464) * wip * Expose whether the encryption key is randomly generated for saved-objects * give feedback to user if encryption key is randomly generated * remove package distributable * update msg for no api integration key * Update x-pack/plugins/encrypted_saved_objects/server/config.ts Co-Authored-By: Brandon Kobel * review II * fix type * rename encryptionKeyRandomlyGenerated -> usingEphemeralEncryptionKey * fix test and mistake Co-authored-by: Brandon Kobel --- .../plugins/encrypted_saved_objects/index.ts | 4 ++- x-pack/legacy/plugins/siem/index.ts | 4 +++ .../rules/use_pre_packaged_rules.tsx | 13 ++++++++- .../detection_engine/signals/types.ts | 1 + .../signals/use_privilege_user.tsx | 13 ++++++++- .../no_api_integration_callout/index.tsx | 26 +++++++++++++++++ .../translations.ts | 28 +++++++++++++++++++ .../components/user_info/index.tsx | 24 +++++++++++++++- .../detection_engine/detection_engine.tsx | 14 +++++++--- .../detection_engine/rules/create/index.tsx | 8 ++---- .../detection_engine/rules/details/index.tsx | 13 ++++----- .../detection_engine/rules/edit/index.tsx | 9 ++---- .../pages/detection_engine/rules/helpers.tsx | 10 +++++++ .../pages/detection_engine/rules/index.tsx | 13 ++++----- .../routes/__mocks__/request_responses.ts | 1 + .../privileges/read_privileges_route.ts | 2 ++ x-pack/legacy/plugins/siem/server/types.ts | 1 + .../server/config.test.ts | 22 +++++++++++++-- .../encrypted_saved_objects/server/config.ts | 11 ++++---- .../encrypted_saved_objects/server/mocks.ts | 1 + .../server/plugin.test.ts | 1 + .../encrypted_saved_objects/server/plugin.ts | 4 ++- 22 files changed, 180 insertions(+), 43 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/components/no_api_integration_callout/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/components/no_api_integration_callout/translations.ts diff --git a/x-pack/legacy/plugins/encrypted_saved_objects/index.ts b/x-pack/legacy/plugins/encrypted_saved_objects/index.ts index 85aa5c22135b6c..69058a7a33f59f 100644 --- a/x-pack/legacy/plugins/encrypted_saved_objects/index.ts +++ b/x-pack/legacy/plugins/encrypted_saved_objects/index.ts @@ -21,7 +21,9 @@ export const encryptedSavedObjects = (kibana: { // Some legacy plugins still use `enabled` config key, so we keep it here, but the rest of the // keys is handled by the New Platform plugin. config: (Joi: Root) => - Joi.object({ enabled: Joi.boolean().default(true) }) + Joi.object({ + enabled: Joi.boolean().default(true), + }) .unknown(true) .default(), diff --git a/x-pack/legacy/plugins/siem/index.ts b/x-pack/legacy/plugins/siem/index.ts index cd9b7f59226b0b..0a3e447ac64a1c 100644 --- a/x-pack/legacy/plugins/siem/index.ts +++ b/x-pack/legacy/plugins/siem/index.ts @@ -5,6 +5,7 @@ */ import { i18n } from '@kbn/i18n'; +import { get } from 'lodash/fp'; import { resolve } from 'path'; import { Server } from 'hapi'; import { Root } from 'joi'; @@ -155,6 +156,9 @@ export const siem = (kibana: any) => { const initializerContext = { ...coreContext, env } as PluginInitializerContext; const serverFacade = { config, + usingEphemeralEncryptionKey: + get('usingEphemeralEncryptionKey', newPlatform.setup.plugins.encryptedSavedObjects) ?? + false, plugins: { alerting: plugins.alerting, actions: newPlatform.start.plugins.actions, diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_pre_packaged_rules.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_pre_packaged_rules.tsx index 14d40f9ffbc37a..d77d6283692a24 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_pre_packaged_rules.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_pre_packaged_rules.tsx @@ -29,6 +29,7 @@ interface UsePrePackagedRuleProps { hasIndexWrite: boolean | null; hasManageApiKey: boolean | null; isAuthenticated: boolean | null; + hasEncryptionKey: boolean | null; isSignalIndexExists: boolean | null; } @@ -38,6 +39,7 @@ interface UsePrePackagedRuleProps { * @param hasIndexWrite boolean * @param hasManageApiKey boolean * @param isAuthenticated boolean + * @param hasEncryptionKey boolean * @param isSignalIndexExists boolean * */ @@ -46,6 +48,7 @@ export const usePrePackagedRules = ({ hasIndexWrite, hasManageApiKey, isAuthenticated, + hasEncryptionKey, isSignalIndexExists, }: UsePrePackagedRuleProps): Return => { const [rulesStatus, setRuleStatus] = useState< @@ -117,6 +120,7 @@ export const usePrePackagedRules = ({ hasIndexWrite && hasManageApiKey && isAuthenticated && + hasEncryptionKey && isSignalIndexExists ) { setLoadingCreatePrePackagedRules(true); @@ -180,7 +184,14 @@ export const usePrePackagedRules = ({ isSubscribed = false; abortCtrl.abort(); }; - }, [canUserCRUD, hasIndexWrite, hasManageApiKey, isAuthenticated, isSignalIndexExists]); + }, [ + canUserCRUD, + hasIndexWrite, + hasManageApiKey, + isAuthenticated, + hasEncryptionKey, + isSignalIndexExists, + ]); return { loading, diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/types.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/types.ts index ea4860dafd40f2..752de13567e5c8 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/types.ts +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/types.ts @@ -97,4 +97,5 @@ export interface Privilege { }; }; is_authenticated: boolean; + has_encryption_key: boolean; } diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_privilege_user.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_privilege_user.tsx index b93009c8ce2c28..55f3386b503d88 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_privilege_user.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_privilege_user.tsx @@ -14,6 +14,7 @@ import * as i18n from './translations'; interface Return { loading: boolean; isAuthenticated: boolean | null; + hasEncryptionKey: boolean | null; hasIndexManage: boolean | null; hasManageApiKey: boolean | null; hasIndexWrite: boolean | null; @@ -25,9 +26,17 @@ interface Return { export const usePrivilegeUser = (): Return => { const [loading, setLoading] = useState(true); const [privilegeUser, setPrivilegeUser] = useState< - Pick + Pick< + Return, + | 'isAuthenticated' + | 'hasEncryptionKey' + | 'hasIndexManage' + | 'hasManageApiKey' + | 'hasIndexWrite' + > >({ isAuthenticated: null, + hasEncryptionKey: null, hasIndexManage: null, hasManageApiKey: null, hasIndexWrite: null, @@ -50,6 +59,7 @@ export const usePrivilegeUser = (): Return => { const indexName = Object.keys(privilege.index)[0]; setPrivilegeUser({ isAuthenticated: privilege.is_authenticated, + hasEncryptionKey: privilege.has_encryption_key, hasIndexManage: privilege.index[indexName].manage, hasIndexWrite: privilege.index[indexName].create || @@ -67,6 +77,7 @@ export const usePrivilegeUser = (): Return => { if (isSubscribed) { setPrivilegeUser({ isAuthenticated: false, + hasEncryptionKey: false, hasIndexManage: false, hasManageApiKey: false, hasIndexWrite: false, diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/no_api_integration_callout/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/no_api_integration_callout/index.tsx new file mode 100644 index 00000000000000..2d517717ac59d5 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/no_api_integration_callout/index.tsx @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiCallOut, EuiButton } from '@elastic/eui'; +import React, { memo, useCallback, useState } from 'react'; + +import * as i18n from './translations'; + +const NoApiIntegrationKeyCallOutComponent = () => { + const [showCallOut, setShowCallOut] = useState(true); + const handleCallOut = useCallback(() => setShowCallOut(false), [setShowCallOut]); + + return showCallOut ? ( + +

{i18n.NO_API_INTEGRATION_KEY_CALLOUT_MSG}

+ + {i18n.DISMISS_CALLOUT} + +
+ ) : null; +}; + +export const NoApiIntegrationKeyCallOut = memo(NoApiIntegrationKeyCallOutComponent); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/no_api_integration_callout/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/no_api_integration_callout/translations.ts new file mode 100644 index 00000000000000..84804af8840f91 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/no_api_integration_callout/translations.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const NO_API_INTEGRATION_KEY_CALLOUT_TITLE = i18n.translate( + 'xpack.siem.detectionEngine.noApiIntegrationKeyCallOutTitle', + { + defaultMessage: 'API integration key required', + } +); + +export const NO_API_INTEGRATION_KEY_CALLOUT_MSG = i18n.translate( + 'xpack.siem.detectionEngine.noApiIntegrationKeyCallOutMsg', + { + defaultMessage: `A new encryption key is generated for saved objects each time you start Kibana. Without a persistent key, you cannot delete or modify rules after Kibana restarts. To set a persistent key, add the xpack.encryptedSavedObjects.encryptionKey setting with any text value of 32 or more characters to the kibana.yml file.`, + } +); + +export const DISMISS_CALLOUT = i18n.translate( + 'xpack.siem.detectionEngine.dismissNoApiIntegrationKeyButton', + { + defaultMessage: 'Dismiss', + } +); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/user_info/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/user_info/index.tsx index 0f6a51e52cd2e8..a96913f2ad541f 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/user_info/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/user_info/index.tsx @@ -18,6 +18,7 @@ export interface State { hasManageApiKey: boolean | null; isSignalIndexExists: boolean | null; isAuthenticated: boolean | null; + hasEncryptionKey: boolean | null; loading: boolean; signalIndexName: string | null; } @@ -29,6 +30,7 @@ const initialState: State = { hasManageApiKey: null, isSignalIndexExists: null, isAuthenticated: null, + hasEncryptionKey: null, loading: true, signalIndexName: null, }; @@ -55,6 +57,10 @@ export type Action = type: 'updateIsAuthenticated'; isAuthenticated: boolean | null; } + | { + type: 'updateHasEncryptionKey'; + hasEncryptionKey: boolean | null; + } | { type: 'updateCanUserCRUD'; canUserCRUD: boolean | null; @@ -102,6 +108,12 @@ export const userInfoReducer = (state: State, action: Action): State => { isAuthenticated: action.isAuthenticated, }; } + case 'updateHasEncryptionKey': { + return { + ...state, + hasEncryptionKey: action.hasEncryptionKey, + }; + } case 'updateCanUserCRUD': { return { ...state, @@ -142,6 +154,7 @@ export const useUserInfo = (): State => { hasManageApiKey, isSignalIndexExists, isAuthenticated, + hasEncryptionKey, loading, signalIndexName, }, @@ -150,6 +163,7 @@ export const useUserInfo = (): State => { const { loading: privilegeLoading, isAuthenticated: isApiAuthenticated, + hasEncryptionKey: isApiEncryptionKey, hasIndexManage: hasApiIndexManage, hasIndexWrite: hasApiIndexWrite, hasManageApiKey: hasApiManageApiKey, @@ -205,6 +219,12 @@ export const useUserInfo = (): State => { } }, [loading, isAuthenticated, isApiAuthenticated]); + useEffect(() => { + if (!loading && hasEncryptionKey !== isApiEncryptionKey && isApiEncryptionKey != null) { + dispatch({ type: 'updateHasEncryptionKey', hasEncryptionKey: isApiEncryptionKey }); + } + }, [loading, hasEncryptionKey, isApiEncryptionKey]); + useEffect(() => { if (!loading && canUserCRUD !== capabilitiesCanUserCRUD && capabilitiesCanUserCRUD != null) { dispatch({ type: 'updateCanUserCRUD', canUserCRUD: capabilitiesCanUserCRUD }); @@ -220,6 +240,7 @@ export const useUserInfo = (): State => { useEffect(() => { if ( isAuthenticated && + hasEncryptionKey && hasIndexManage && isSignalIndexExists != null && !isSignalIndexExists && @@ -227,12 +248,13 @@ export const useUserInfo = (): State => { ) { createSignalIndex(); } - }, [createSignalIndex, isAuthenticated, isSignalIndexExists, hasIndexManage]); + }, [createSignalIndex, isAuthenticated, hasEncryptionKey, isSignalIndexExists, hasIndexManage]); return { loading, isSignalIndexExists, isAuthenticated, + hasEncryptionKey, canUserCRUD, hasIndexManage, hasIndexWrite, diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx index b6ddb4de9fd390..d854c377e6ec86 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx @@ -18,7 +18,10 @@ import { GlobalTime } from '../../containers/global_time'; import { indicesExistOrDataTemporarilyUnavailable, WithSource } from '../../containers/source'; import { AlertsTable } from '../../components/alerts_viewer/alerts_table'; import { FiltersGlobal } from '../../components/filters_global'; -import { DETECTION_ENGINE_PAGE_NAME } from '../../components/link_to/redirect_to_detection_engine'; +import { + getDetectionEngineTabUrl, + getRulesUrl, +} from '../../components/link_to/redirect_to_detection_engine'; import { SiemSearchBar } from '../../components/search_bar'; import { WrapperPage } from '../../components/wrapper_page'; import { State } from '../../store'; @@ -30,6 +33,7 @@ import { InputsRange } from '../../store/inputs/model'; import { AlertsByCategory } from '../overview/alerts_by_category'; import { useSignalInfo } from './components/signals_info'; import { SignalsTable } from './components/signals'; +import { NoApiIntegrationKeyCallOut } from './components/no_api_integration_callout'; import { NoWriteSignalsCallOut } from './components/no_write_signals_callout'; import { SignalsHistogramPanel } from './components/signals_histogram_panel'; import { signalsHistogramOptions } from './components/signals_histogram_panel/config'; @@ -79,6 +83,7 @@ const DetectionEnginePageComponent: React.FC loading, isSignalIndexExists, isAuthenticated: isUserAuthenticated, + hasEncryptionKey, canUserCRUD, signalIndexName, hasIndexWrite, @@ -101,7 +106,7 @@ const DetectionEnginePageComponent: React.FC isSelected={tab.id === tabName} disabled={tab.disabled} key={tab.id} - href={`#/${DETECTION_ENGINE_PAGE_NAME}/${tab.id}`} + href={getDetectionEngineTabUrl(tab.id)} > {tab.name} @@ -134,6 +139,7 @@ const DetectionEnginePageComponent: React.FC return ( <> + {hasEncryptionKey != null && !hasEncryptionKey && } {hasIndexWrite != null && !hasIndexWrite && } {({ indicesExist, indexPattern }) => { @@ -155,7 +161,7 @@ const DetectionEnginePageComponent: React.FC } title={i18n.PAGE_TITLE} > - + {i18n.BUTTON_MANAGE_RULES} @@ -184,7 +190,7 @@ const DetectionEnginePageComponent: React.FC { loading, isSignalIndexExists, isAuthenticated, + hasEncryptionKey, canUserCRUD, hasManageApiKey, } = useUserInfo(); @@ -239,11 +241,7 @@ const CreateRulePageComponent: React.FC = () => { return ; } - if ( - isSignalIndexExists != null && - isAuthenticated != null && - (!isSignalIndexExists || !isAuthenticated) - ) { + if (redirectToDetections(isSignalIndexExists, isAuthenticated, hasEncryptionKey)) { return ; } else if (userHasNoPermissions) { return ; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx index 7b615d5f159c2d..bac1494c4fdd81 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx @@ -25,9 +25,9 @@ import { connect } from 'react-redux'; import { FiltersGlobal } from '../../../../components/filters_global'; import { FormattedDate } from '../../../../components/formatted_date'; import { - getDetectionEngineUrl, getEditRuleUrl, getRulesUrl, + DETECTION_ENGINE_PAGE_NAME, } from '../../../../components/link_to/redirect_to_detection_engine'; import { SiemSearchBar } from '../../../../components/search_bar'; import { WrapperPage } from '../../../../components/wrapper_page'; @@ -54,7 +54,7 @@ import * as detectionI18n from '../../translations'; import { ReadOnlyCallOut } from '../components/read_only_callout'; import { RuleSwitch } from '../components/rule_switch'; import { StepPanel } from '../components/step_panel'; -import { getStepsData } from '../helpers'; +import { getStepsData, redirectToDetections } from '../helpers'; import * as ruleI18n from '../translations'; import * as i18n from './translations'; import { GlobalTime } from '../../../../containers/global_time'; @@ -113,6 +113,7 @@ const RuleDetailsPageComponent: FC = ({ loading, isSignalIndexExists, isAuthenticated, + hasEncryptionKey, canUserCRUD, hasManageApiKey, hasIndexWrite, @@ -236,12 +237,8 @@ const RuleDetailsPageComponent: FC = ({ [ruleEnabled, setRuleEnabled] ); - if ( - isSignalIndexExists != null && - isAuthenticated != null && - (!isSignalIndexExists || !isAuthenticated) - ) { - return ; + if (redirectToDetections(isSignalIndexExists, isAuthenticated, hasEncryptionKey)) { + return ; } return ( diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx index 65f4bd2edf7cd0..99fcff6b8d2fda 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx @@ -32,7 +32,7 @@ import { StepAboutRule } from '../components/step_about_rule'; import { StepDefineRule } from '../components/step_define_rule'; import { StepScheduleRule } from '../components/step_schedule_rule'; import { formatRule } from '../create/helpers'; -import { getStepsData } from '../helpers'; +import { getStepsData, redirectToDetections } from '../helpers'; import * as ruleI18n from '../translations'; import { RuleStep, DefineStepRule, AboutStepRule, ScheduleStepRule } from '../types'; import * as i18n from './translations'; @@ -56,6 +56,7 @@ const EditRulePageComponent: FC = () => { loading: initLoading, isSignalIndexExists, isAuthenticated, + hasEncryptionKey, canUserCRUD, hasManageApiKey, } = useUserInfo(); @@ -270,11 +271,7 @@ const EditRulePageComponent: FC = () => { return ; } - if ( - isSignalIndexExists != null && - isAuthenticated != null && - (!isSignalIndexExists || !isAuthenticated) - ) { + if (redirectToDetections(isSignalIndexExists, isAuthenticated, hasEncryptionKey)) { return ; } else if (userHasNoPermissions) { return ; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx index ce0d50d9b61065..4e98fc17404c92 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx @@ -138,3 +138,13 @@ export const setFieldValue = ( form.setFieldValue(key, val); } }); + +export const redirectToDetections = ( + isSignalIndexExists: boolean | null, + isAuthenticated: boolean | null, + hasEncryptionKey: boolean | null +) => + isSignalIndexExists != null && + isAuthenticated != null && + hasEncryptionKey != null && + (!isSignalIndexExists || !isAuthenticated || !hasEncryptionKey); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/index.tsx index 1c0ed34e927938..0c53ad19a35747 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/index.tsx @@ -10,6 +10,7 @@ import { Redirect } from 'react-router-dom'; import { usePrePackagedRules } from '../../../containers/detection_engine/rules'; import { + DETECTION_ENGINE_PAGE_NAME, getDetectionEngineUrl, getCreateRuleUrl, } from '../../../components/link_to/redirect_to_detection_engine'; @@ -22,7 +23,7 @@ import { AllRules } from './all'; import { ImportRuleModal } from './components/import_rule_modal'; import { ReadOnlyCallOut } from './components/read_only_callout'; import { UpdatePrePackagedRulesCallOut } from './components/pre_packaged_rules/update_callout'; -import { getPrePackagedRuleStatus } from './helpers'; +import { getPrePackagedRuleStatus, redirectToDetections } from './helpers'; import * as i18n from './translations'; type Func = () => void; @@ -35,6 +36,7 @@ const RulesPageComponent: React.FC = () => { loading, isSignalIndexExists, isAuthenticated, + hasEncryptionKey, canUserCRUD, hasIndexWrite, hasManageApiKey, @@ -54,6 +56,7 @@ const RulesPageComponent: React.FC = () => { hasManageApiKey, isSignalIndexExists, isAuthenticated, + hasEncryptionKey, }); const prePackagedRuleStatus = getPrePackagedRuleStatus( rulesInstalled, @@ -83,12 +86,8 @@ const RulesPageComponent: React.FC = () => { refreshRulesData.current = refreshRule; }, []); - if ( - isSignalIndexExists != null && - isAuthenticated != null && - (!isSignalIndexExists || !isAuthenticated) - ) { - return ; + if (redirectToDetections(isSignalIndexExists, isAuthenticated, hasEncryptionKey)) { + return ; } return ( diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts index eea25a1e89cc84..6a42aed123fa3c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts @@ -390,6 +390,7 @@ export const getMockPrivileges = () => ({ }, application: {}, is_authenticated: false, + has_encryption_key: true, }); export const getFindResultStatus = (): SavedObjectsFindResponse => ({ diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts index 803d9d645aadbe..5ea4dc7595b2b1 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts @@ -29,8 +29,10 @@ export const createReadPrivilegesRulesRoute = (server: ServerFacade): Hapi.Serve const callWithRequest = callWithRequestFactory(request, server); const index = getIndex(request, server); const permissions = await readPrivileges(callWithRequest, index); + const usingEphemeralEncryptionKey = server.usingEphemeralEncryptionKey; return merge(permissions, { is_authenticated: request?.auth?.isAuthenticated ?? false, + has_encryption_key: !usingEphemeralEncryptionKey, }); } catch (err) { return transformError(err); diff --git a/x-pack/legacy/plugins/siem/server/types.ts b/x-pack/legacy/plugins/siem/server/types.ts index 3fa2268afe92c3..7c07e63404eaa6 100644 --- a/x-pack/legacy/plugins/siem/server/types.ts +++ b/x-pack/legacy/plugins/siem/server/types.ts @@ -8,6 +8,7 @@ import { Legacy } from 'kibana'; export interface ServerFacade { config: Legacy.Server['config']; + usingEphemeralEncryptionKey: boolean; plugins: { // eslint-disable-next-line @typescript-eslint/no-explicit-any actions: any; // We have to do this at the moment because the types are not compatible diff --git a/x-pack/plugins/encrypted_saved_objects/server/config.test.ts b/x-pack/plugins/encrypted_saved_objects/server/config.test.ts index 7d6632aa56cb18..e05d8d687d05a3 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/config.test.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/config.test.ts @@ -49,7 +49,7 @@ describe('config schema', () => { }); describe('createConfig$()', () => { - it('should log a warning and set xpack.encryptedSavedObjects.encryptionKey if not set', async () => { + it('should log a warning, set xpack.encryptedSavedObjects.encryptionKey and usingEphemeralEncryptionKey=true when encryptionKey is not set', async () => { const mockRandomBytes = jest.requireMock('crypto').randomBytes; mockRandomBytes.mockReturnValue('ab'.repeat(16)); @@ -57,7 +57,10 @@ describe('createConfig$()', () => { const config = await createConfig$(contextMock) .pipe(first()) .toPromise(); - expect(config).toEqual({ encryptionKey: 'ab'.repeat(16) }); + expect(config).toEqual({ + config: { encryptionKey: 'ab'.repeat(16) }, + usingEphemeralEncryptionKey: true, + }); expect(loggingServiceMock.collect(contextMock.logger).warn).toMatchInlineSnapshot(` Array [ @@ -67,4 +70,19 @@ describe('createConfig$()', () => { ] `); }); + + it('should not log a warning and set usingEphemeralEncryptionKey=false when encryptionKey is set', async () => { + const contextMock = coreMock.createPluginInitializerContext({ + encryptionKey: 'supersecret', + }); + const config = await createConfig$(contextMock) + .pipe(first()) + .toPromise(); + expect(config).toEqual({ + config: { encryptionKey: 'supersecret' }, + usingEphemeralEncryptionKey: false, + }); + + expect(loggingServiceMock.collect(contextMock.logger).warn).toEqual([]); + }); }); diff --git a/x-pack/plugins/encrypted_saved_objects/server/config.ts b/x-pack/plugins/encrypted_saved_objects/server/config.ts index c755b7dd9f205f..2f018505207242 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/config.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/config.ts @@ -5,15 +5,10 @@ */ import crypto from 'crypto'; -import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; import { schema, TypeOf } from '@kbn/config-schema'; import { PluginInitializerContext } from 'src/core/server'; -export type ConfigType = ReturnType extends Observable - ? P - : ReturnType; - export const ConfigSchema = schema.object({ enabled: schema.boolean({ defaultValue: true }), encryptionKey: schema.conditional( @@ -30,6 +25,7 @@ export function createConfig$(context: PluginInitializerContext) { const logger = context.logger.get('config'); let encryptionKey = config.encryptionKey; + const usingEphemeralEncryptionKey = encryptionKey === undefined; if (encryptionKey === undefined) { logger.warn( 'Generating a random key for xpack.encryptedSavedObjects.encryptionKey. ' + @@ -40,7 +36,10 @@ export function createConfig$(context: PluginInitializerContext) { encryptionKey = crypto.randomBytes(16).toString('hex'); } - return { ...config, encryptionKey }; + return { + config: { ...config, encryptionKey }, + usingEphemeralEncryptionKey, + }; }) ); } diff --git a/x-pack/plugins/encrypted_saved_objects/server/mocks.ts b/x-pack/plugins/encrypted_saved_objects/server/mocks.ts index 87c36381a841aa..7f53f47760f129 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/mocks.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/mocks.ts @@ -10,6 +10,7 @@ function createEncryptedSavedObjectsSetupMock() { return { registerType: jest.fn(), __legacyCompat: { registerLegacyAPI: jest.fn() }, + usingEphemeralEncryptionKey: true, } as jest.Mocked; } diff --git a/x-pack/plugins/encrypted_saved_objects/server/plugin.test.ts b/x-pack/plugins/encrypted_saved_objects/server/plugin.test.ts index 534ed13ba0acbd..5228734e4a7732 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/plugin.test.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/plugin.test.ts @@ -18,6 +18,7 @@ describe('EncryptedSavedObjects Plugin', () => { "registerLegacyAPI": [Function], }, "registerType": [Function], + "usingEphemeralEncryptionKey": true, } `); }); diff --git a/x-pack/plugins/encrypted_saved_objects/server/plugin.ts b/x-pack/plugins/encrypted_saved_objects/server/plugin.ts index ecd917ff90d003..d9185251ca4664 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/plugin.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/plugin.ts @@ -23,6 +23,7 @@ import { SavedObjectsSetup, setupSavedObjects } from './saved_objects'; export interface PluginSetupContract { registerType: (typeRegistration: EncryptedSavedObjectTypeRegistration) => void; __legacyCompat: { registerLegacyAPI: (legacyAPI: LegacyAPI) => void }; + usingEphemeralEncryptionKey: boolean; } export interface PluginStartContract extends SavedObjectsSetup { @@ -59,7 +60,7 @@ export class Plugin { } public async setup(core: CoreSetup): Promise { - const config = await createConfig$(this.initializerContext) + const { config, usingEphemeralEncryptionKey } = await createConfig$(this.initializerContext) .pipe(first()) .toPromise(); @@ -81,6 +82,7 @@ export class Plugin { registerType: (typeRegistration: EncryptedSavedObjectTypeRegistration) => service.registerType(typeRegistration), __legacyCompat: { registerLegacyAPI: (legacyAPI: LegacyAPI) => (this.legacyAPI = legacyAPI) }, + usingEphemeralEncryptionKey, }; } From 7b876c05f40f40a2df7b228d77ec5d68d8a2f241 Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Sat, 1 Feb 2020 08:03:51 -0500 Subject: [PATCH 18/33] fix 508 (#56520) --- .../components/signals/actions.tsx | 78 ++++++++++--------- .../routes/rules/update_rules_route.ts | 4 +- 2 files changed, 44 insertions(+), 38 deletions(-) diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/actions.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/actions.tsx index a33efeda2196b5..003d2baa53dbc1 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/actions.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/actions.tsx @@ -5,7 +5,7 @@ */ import dateMath from '@elastic/datemath'; -import { getOr } from 'lodash/fp'; +import { getOr, isEmpty } from 'lodash/fp'; import moment from 'moment'; import { updateSignalStatus } from '../../../../containers/detection_engine/signals/api'; @@ -78,6 +78,7 @@ export const sendSignalToTimelineAction = async ({ ecsData, updateTimelineIsLoading, }: SendSignalToTimelineActionProps) => { + let openSignalInBasicTimeline = true; const timelineId = ecsData.signal?.rule?.timeline_id != null ? ecsData.signal?.rule?.timeline_id[0] : ''; @@ -105,52 +106,57 @@ export const sendSignalToTimelineAction = async ({ id: timelineId, }, }); - const timelineTemplate: TimelineResult = omitTypenameInTimeline( getOr({}, 'data.getOneTimeline', responseTimeline) ); - const { timeline } = formatTimelineResultToModel(timelineTemplate, true); - const query = replaceTemplateFieldFromQuery( - timeline.kqlQuery?.filterQuery?.kuery?.expression ?? '', - ecsData - ); - const filters = replaceTemplateFieldFromMatchFilters(timeline.filters ?? [], ecsData); - const dataProviders = replaceTemplateFieldFromDataProviders( - timeline.dataProviders ?? [], - ecsData - ); - createTimeline({ - from, - timeline: { - ...timeline, - dataProviders, - eventType: 'all', - filters, - dateRange: { - start: from, - end: to, - }, - kqlQuery: { - filterQuery: { - kuery: { + if (!isEmpty(timelineTemplate)) { + openSignalInBasicTimeline = false; + const { timeline } = formatTimelineResultToModel(timelineTemplate, true); + const query = replaceTemplateFieldFromQuery( + timeline.kqlQuery?.filterQuery?.kuery?.expression ?? '', + ecsData + ); + const filters = replaceTemplateFieldFromMatchFilters(timeline.filters ?? [], ecsData); + const dataProviders = replaceTemplateFieldFromDataProviders( + timeline.dataProviders ?? [], + ecsData + ); + createTimeline({ + from, + timeline: { + ...timeline, + dataProviders, + eventType: 'all', + filters, + dateRange: { + start: from, + end: to, + }, + kqlQuery: { + filterQuery: { + kuery: { + kind: timeline.kqlQuery?.filterQuery?.kuery?.kind ?? 'kuery', + expression: query, + }, + serializedQuery: convertKueryToElasticSearchQuery(query), + }, + filterQueryDraft: { kind: timeline.kqlQuery?.filterQuery?.kuery?.kind ?? 'kuery', expression: query, }, - serializedQuery: convertKueryToElasticSearchQuery(query), - }, - filterQueryDraft: { - kind: timeline.kqlQuery?.filterQuery?.kuery?.kind ?? 'kuery', - expression: query, }, + show: true, }, - show: true, - }, - to, - }); + to, + }); + } } catch { + openSignalInBasicTimeline = true; updateTimelineIsLoading({ id: 'timeline-1', isLoading: false }); } - } else { + } + + if (openSignalInBasicTimeline) { createTimeline({ from, timeline: { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts index f51cea0753f1ab..590307e06a26a9 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts @@ -39,8 +39,8 @@ export const createUpdateRulesRoute = (server: ServerFacade): Hapi.ServerRoute = language, output_index: outputIndex, saved_id: savedId, - timeline_id: timelineId, - timeline_title: timelineTitle, + timeline_id: timelineId = null, + timeline_title: timelineTitle = null, meta, filters, rule_id: ruleId, From 27a4fe2ef4632d743e266925d04f6eb0dac958ea Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Sat, 1 Feb 2020 16:38:30 +0100 Subject: [PATCH 19/33] [Console] Fix use of legacy config (#56468) * Fix use of legacy config * Add types for ssl --- .../console/server/lib/elasticsearch_proxy_config.ts | 7 ++++--- src/plugins/console/server/plugin.ts | 4 +--- src/plugins/console/server/types.ts | 8 ++++++++ 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/plugins/console/server/lib/elasticsearch_proxy_config.ts b/src/plugins/console/server/lib/elasticsearch_proxy_config.ts index 901d726ac51d82..28a971794d403d 100644 --- a/src/plugins/console/server/lib/elasticsearch_proxy_config.ts +++ b/src/plugins/console/server/lib/elasticsearch_proxy_config.ts @@ -21,9 +21,10 @@ import _ from 'lodash'; import http from 'http'; import https from 'https'; import url from 'url'; -import { Duration } from 'moment'; -const createAgent = (legacyConfig: any) => { +import { ESConfigForProxy } from '../types'; + +const createAgent = (legacyConfig: ESConfigForProxy) => { const target = url.parse(_.head(legacyConfig.hosts)); if (!/^https/.test(target.protocol || '')) return new http.Agent(); @@ -59,7 +60,7 @@ const createAgent = (legacyConfig: any) => { return new https.Agent(agentOptions); }; -export const getElasticsearchProxyConfig = (legacyConfig: { requestTimeout: Duration }) => { +export const getElasticsearchProxyConfig = (legacyConfig: ESConfigForProxy) => { return { timeout: legacyConfig.requestTimeout.asMilliseconds(), agent: createAgent(legacyConfig), diff --git a/src/plugins/console/server/plugin.ts b/src/plugins/console/server/plugin.ts index c8ef84aee3b615..65647bd5acb7c5 100644 --- a/src/plugins/console/server/plugin.ts +++ b/src/plugins/console/server/plugin.ts @@ -60,9 +60,7 @@ export class ConsoleServerPlugin implements Plugin { const legacyConfig = readLegacyEsConfig(); return { ...elasticsearch, - hosts: legacyConfig.hosts, - requestHeadersWhitelist: legacyConfig.requestHeadersWhitelist, - customHeaders: legacyConfig.customHeaders, + ...legacyConfig, }; }, pathFilters: proxyPathFilters, diff --git a/src/plugins/console/server/types.ts b/src/plugins/console/server/types.ts index 60ce56ad39fcd4..adafcd4d305269 100644 --- a/src/plugins/console/server/types.ts +++ b/src/plugins/console/server/types.ts @@ -31,4 +31,12 @@ export interface ESConfigForProxy { requestHeadersWhitelist: string[]; customHeaders: Record; requestTimeout: Duration; + ssl?: { + verificationMode: 'none' | 'certificate' | 'full'; + certificateAuthorities: string[] | string; + alwaysPresentCertificate: boolean; + certificate?: string; + key?: string; + keyPassphrase?: string; + }; } From cc9ff6da56b1e5accc828615bbeecfbf48d0b4d8 Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Mon, 3 Feb 2020 09:57:55 +0100 Subject: [PATCH 20/33] avoid 'app not found' flickering while awaiting for mount (#56483) --- .../application/ui/app_container.test.tsx | 88 +++++++++++++++++++ .../public/application/ui/app_container.tsx | 18 ++-- 2 files changed, 98 insertions(+), 8 deletions(-) create mode 100644 src/core/public/application/ui/app_container.test.tsx diff --git a/src/core/public/application/ui/app_container.test.tsx b/src/core/public/application/ui/app_container.test.tsx new file mode 100644 index 00000000000000..a46243a2da4939 --- /dev/null +++ b/src/core/public/application/ui/app_container.test.tsx @@ -0,0 +1,88 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { mount } from 'enzyme'; + +import { AppContainer } from './app_container'; +import { Mounter, AppMountParameters, AppStatus } from '../types'; + +describe('AppContainer', () => { + const appId = 'someApp'; + const setAppLeaveHandler = jest.fn(); + + const flushPromises = async () => { + await new Promise(async resolve => { + setImmediate(() => resolve()); + }); + }; + + const createResolver = (): [Promise, () => void] => { + let resolve: () => void | undefined; + const promise = new Promise(r => { + resolve = r; + }); + return [promise, resolve!]; + }; + + const createMounter = (promise: Promise): Mounter => ({ + appBasePath: '/base-path', + appRoute: '/some-route', + unmountBeforeMounting: false, + mount: async ({ element }: AppMountParameters) => { + await promise; + const container = document.createElement('div'); + container.innerHTML = 'some-content'; + element.appendChild(container); + return () => container.remove(); + }, + }); + + it('should hide the "not found" page before mounting the route', async () => { + const [waitPromise, resolvePromise] = createResolver(); + const mounter = createMounter(waitPromise); + + const wrapper = mount( + + ); + + expect(wrapper.text()).toContain('Application Not Found'); + + wrapper.setProps({ + appId, + setAppLeaveHandler, + mounter, + appStatus: AppStatus.accessible, + }); + wrapper.update(); + + expect(wrapper.text()).toEqual(''); + + resolvePromise(); + await flushPromises(); + wrapper.update(); + + expect(wrapper.text()).toContain('some-content'); + }); +}); diff --git a/src/core/public/application/ui/app_container.tsx b/src/core/public/application/ui/app_container.tsx index 66c837d238276c..885157843e7df8 100644 --- a/src/core/public/application/ui/app_container.tsx +++ b/src/core/public/application/ui/app_container.tsx @@ -53,25 +53,27 @@ export const AppContainer: FunctionComponent = ({ unmountRef.current = null; } }; - const mount = async () => { - if (!mounter || appStatus !== AppStatus.accessible) { - return setAppNotFound(true); - } - if (mounter.unmountBeforeMounting) { - unmount(); - } + if (!mounter || appStatus !== AppStatus.accessible) { + return setAppNotFound(true); + } + setAppNotFound(false); + if (mounter.unmountBeforeMounting) { + unmount(); + } + + const mount = async () => { unmountRef.current = (await mounter.mount({ appBasePath: mounter.appBasePath, element: elementRef.current!, onAppLeave: handler => setAppLeaveHandler(appId, handler), })) || null; - setAppNotFound(false); }; mount(); + return unmount; }, [appId, appStatus, mounter, setAppLeaveHandler]); From fe86a86d14718ab510d60ecb1c091fefb83d3695 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Mon, 3 Feb 2020 11:02:29 +0000 Subject: [PATCH 21/33] adds the AlertDetails page (#55671) This PR adds an Alerts Details page, linked from the Alerts List. It includes: Header containing details about the Alert. Quick Enable / Mute buttons for the Alert Disabled buttons to the _Edit Alert+ flyout (waiting on #51545), View in App (waiting on #56298) and Activity Log (waiting on #51548) --- .../np_ready/public/application/app.tsx | 10 +- .../public/application/constants/index.ts | 1 + .../lib/action_connector_api.test.ts | 1 + .../public/application/lib/alert_api.test.ts | 101 ++++ .../public/application/lib/alert_api.ts | 40 +- .../application/lib/value_validators.test.ts | 77 +++ .../application/lib/value_validators.ts | 33 ++ .../action_connector_form.test.tsx | 6 +- .../action_type_menu.test.tsx | 4 +- .../connector_add_flyout.test.tsx | 4 +- .../connector_edit_flyout.test.tsx | 2 +- .../components/alert_details.test.tsx | 519 ++++++++++++++++++ .../components/alert_details.tsx | 176 ++++++ .../components/alert_details_route.test.tsx | 409 ++++++++++++++ .../components/alert_details_route.tsx | 118 ++++ .../components/alerts_list.test.tsx | 9 +- .../alerts_list/components/alerts_list.tsx | 45 +- .../components/collapsed_item_actions.tsx | 32 +- .../components/alert_quick_edit_buttons.tsx} | 189 +++---- .../components/bulk_operation_popover.tsx | 42 ++ .../with_actions_api_operations.test.tsx | 51 ++ .../with_actions_api_operations.tsx | 28 + .../with_bulk_alert_api_operations.test.tsx | 269 +++++++++ .../with_bulk_alert_api_operations.tsx | 103 ++++ .../np_ready/public/types.ts | 12 +- .../apps/triggers_actions_ui/details.ts | 146 +++++ .../apps/triggers_actions_ui/home_page.ts | 38 ++ .../apps/triggers_actions_ui/index.ts | 1 + .../fixtures/plugins/alerts/index.ts | 43 +- .../page_objects/alert_details.ts | 26 + .../page_objects/index.ts | 2 + .../page_objects/triggers_actions_ui_page.ts | 4 + .../services/alerting/actions.ts | 48 ++ .../services/alerting/alerts.ts | 79 +++ .../services/alerting/index.ts | 22 + .../functional_with_es_ssl/services/index.ts | 2 + 36 files changed, 2527 insertions(+), 165 deletions(-) create mode 100644 x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/value_validators.test.ts create mode 100644 x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/value_validators.ts create mode 100644 x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alert_details/components/alert_details.test.tsx create mode 100644 x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alert_details/components/alert_details.tsx create mode 100644 x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alert_details/components/alert_details_route.test.tsx create mode 100644 x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alert_details/components/alert_details_route.tsx rename x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/{alerts_list/components/bulk_action_popover.tsx => common/components/alert_quick_edit_buttons.tsx} (52%) create mode 100644 x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/bulk_operation_popover.tsx create mode 100644 x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/with_actions_api_operations.test.tsx create mode 100644 x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/with_actions_api_operations.tsx create mode 100644 x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/with_bulk_alert_api_operations.test.tsx create mode 100644 x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/with_bulk_alert_api_operations.tsx create mode 100644 x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts create mode 100644 x-pack/test/functional_with_es_ssl/page_objects/alert_details.ts create mode 100644 x-pack/test/functional_with_es_ssl/services/alerting/actions.ts create mode 100644 x-pack/test/functional_with_es_ssl/services/alerting/alerts.ts create mode 100644 x-pack/test/functional_with_es_ssl/services/alerting/index.ts diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/app.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/app.tsx index 57e6fc4a9e18b1..7d9a963c9c6b32 100644 --- a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/app.tsx +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/app.tsx @@ -13,12 +13,13 @@ import { IUiSettingsClient, ApplicationStart, } from 'kibana/public'; -import { BASE_PATH, Section } from './constants'; +import { BASE_PATH, Section, routeToAlertDetails } from './constants'; import { TriggersActionsUIHome } from './home'; import { AppContextProvider, useAppDependencies } from './app_context'; import { hasShowAlertsCapability } from './lib/capabilities'; import { LegacyDependencies, ActionTypeModel, AlertTypeModel } from '../types'; import { TypeRegistry } from './type_registry'; +import { AlertDetailsRouteWithApi as AlertDetailsRoute } from './sections/alert_details/components/alert_details_route'; export interface AppDeps { chrome: ChromeStart; @@ -53,11 +54,8 @@ export const AppWithoutRouter = ({ sectionsRegex }: any) => { const DEFAULT_SECTION: Section = canShowAlerts ? 'alerts' : 'connectors'; return ( - + + {canShowAlerts && } ); diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/constants/index.ts b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/constants/index.ts index a8364ffe21019d..11b094dea0e624 100644 --- a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/constants/index.ts +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/constants/index.ts @@ -13,6 +13,7 @@ export type Section = 'connectors' | 'alerts'; export const routeToHome = `${BASE_PATH}`; export const routeToConnectors = `${BASE_PATH}/connectors`; export const routeToAlerts = `${BASE_PATH}/alerts`; +export const routeToAlertDetails = `${BASE_PATH}/alert/:alertId`; export { TIME_UNITS } from './time_units'; export enum SORT_ORDERS { diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/action_connector_api.test.ts b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/action_connector_api.test.ts index bc2949917edea7..00a55bb2588bb3 100644 --- a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/action_connector_api.test.ts +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/action_connector_api.test.ts @@ -24,6 +24,7 @@ describe('loadActionTypes', () => { { id: 'test', name: 'Test', + enabled: true, }, ]; http.get.mockResolvedValueOnce(resolvedValue); diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/alert_api.test.ts b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/alert_api.test.ts index 0106970cf9c38c..35d1a095188dee 100644 --- a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/alert_api.test.ts +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/alert_api.test.ts @@ -8,15 +8,22 @@ import { Alert, AlertType } from '../../types'; import { httpServiceMock } from '../../../../../../../../src/core/public/mocks'; import { createAlert, + deleteAlert, deleteAlerts, disableAlerts, enableAlerts, + disableAlert, + enableAlert, + loadAlert, loadAlerts, loadAlertTypes, muteAlerts, unmuteAlerts, + muteAlert, + unmuteAlert, updateAlert, } from './alert_api'; +import uuid from 'uuid'; const http = httpServiceMock.createStartContract(); @@ -42,6 +49,31 @@ describe('loadAlertTypes', () => { }); }); +describe('loadAlert', () => { + test('should call get API with base parameters', async () => { + const alertId = uuid.v4(); + const resolvedValue = { + id: alertId, + name: 'name', + tags: [], + enabled: true, + alertTypeId: '.noop', + schedule: { interval: '1s' }, + actions: [], + params: {}, + createdBy: null, + updatedBy: null, + throttle: null, + muteAll: false, + mutedInstanceIds: [], + }; + http.get.mockResolvedValueOnce(resolvedValue); + + expect(await loadAlert({ http, alertId })).toEqual(resolvedValue); + expect(http.get).toHaveBeenCalledWith(`/api/alert/${alertId}`); + }); +}); + describe('loadAlerts', () => { test('should call find API with base parameters', async () => { const resolvedValue = { @@ -230,6 +262,19 @@ describe('loadAlerts', () => { }); }); +describe('deleteAlert', () => { + test('should call delete API for alert', async () => { + const id = '1'; + const result = await deleteAlert({ http, id }); + expect(result).toEqual(undefined); + expect(http.delete.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "/api/alert/1", + ] + `); + }); +}); + describe('deleteAlerts', () => { test('should call delete API for each alert', async () => { const ids = ['1', '2', '3']; @@ -335,6 +380,62 @@ describe('updateAlert', () => { }); }); +describe('enableAlert', () => { + test('should call enable alert API', async () => { + const result = await enableAlert({ http, id: '1' }); + expect(result).toEqual(undefined); + expect(http.post.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/alert/1/_enable", + ], + ] + `); + }); +}); + +describe('disableAlert', () => { + test('should call disable alert API', async () => { + const result = await disableAlert({ http, id: '1' }); + expect(result).toEqual(undefined); + expect(http.post.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/alert/1/_disable", + ], + ] + `); + }); +}); + +describe('muteAlert', () => { + test('should call mute alert API', async () => { + const result = await muteAlert({ http, id: '1' }); + expect(result).toEqual(undefined); + expect(http.post.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/alert/1/_mute_all", + ], + ] + `); + }); +}); + +describe('unmuteAlert', () => { + test('should call unmute alert API', async () => { + const result = await unmuteAlert({ http, id: '1' }); + expect(result).toEqual(undefined); + expect(http.post.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/alert/1/_unmute_all", + ], + ] + `); + }); +}); + describe('enableAlerts', () => { test('should call enable alert API per alert', async () => { const ids = ['1', '2', '3']; diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/alert_api.ts b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/alert_api.ts index 0b4f5731c13153..acc318bd5fbea5 100644 --- a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/alert_api.ts +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/alert_api.ts @@ -12,6 +12,16 @@ export async function loadAlertTypes({ http }: { http: HttpSetup }): Promise { + return await http.get(`${BASE_ALERT_API_PATH}/${alertId}`); +} + export async function loadAlerts({ http, page, @@ -55,6 +65,10 @@ export async function loadAlerts({ }); } +export async function deleteAlert({ id, http }: { id: string; http: HttpSetup }): Promise { + await http.delete(`${BASE_ALERT_API_PATH}/${id}`); +} + export async function deleteAlerts({ ids, http, @@ -62,7 +76,7 @@ export async function deleteAlerts({ ids: string[]; http: HttpSetup; }): Promise { - await Promise.all(ids.map(id => http.delete(`${BASE_ALERT_API_PATH}/${id}`))); + await Promise.all(ids.map(id => deleteAlert({ http, id }))); } export async function createAlert({ @@ -91,6 +105,10 @@ export async function updateAlert({ }); } +export async function enableAlert({ id, http }: { id: string; http: HttpSetup }): Promise { + await http.post(`${BASE_ALERT_API_PATH}/${id}/_enable`); +} + export async function enableAlerts({ ids, http, @@ -98,7 +116,11 @@ export async function enableAlerts({ ids: string[]; http: HttpSetup; }): Promise { - await Promise.all(ids.map(id => http.post(`${BASE_ALERT_API_PATH}/${id}/_enable`))); + await Promise.all(ids.map(id => enableAlert({ id, http }))); +} + +export async function disableAlert({ id, http }: { id: string; http: HttpSetup }): Promise { + await http.post(`${BASE_ALERT_API_PATH}/${id}/_disable`); } export async function disableAlerts({ @@ -108,11 +130,19 @@ export async function disableAlerts({ ids: string[]; http: HttpSetup; }): Promise { - await Promise.all(ids.map(id => http.post(`${BASE_ALERT_API_PATH}/${id}/_disable`))); + await Promise.all(ids.map(id => disableAlert({ id, http }))); +} + +export async function muteAlert({ id, http }: { id: string; http: HttpSetup }): Promise { + await http.post(`${BASE_ALERT_API_PATH}/${id}/_mute_all`); } export async function muteAlerts({ ids, http }: { ids: string[]; http: HttpSetup }): Promise { - await Promise.all(ids.map(id => http.post(`${BASE_ALERT_API_PATH}/${id}/_mute_all`))); + await Promise.all(ids.map(id => muteAlert({ http, id }))); +} + +export async function unmuteAlert({ id, http }: { id: string; http: HttpSetup }): Promise { + await http.post(`${BASE_ALERT_API_PATH}/${id}/_unmute_all`); } export async function unmuteAlerts({ @@ -122,5 +152,5 @@ export async function unmuteAlerts({ ids: string[]; http: HttpSetup; }): Promise { - await Promise.all(ids.map(id => http.post(`${BASE_ALERT_API_PATH}/${id}/_unmute_all`))); + await Promise.all(ids.map(id => unmuteAlert({ id, http }))); } diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/value_validators.test.ts b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/value_validators.test.ts new file mode 100644 index 00000000000000..90f575d9391b32 --- /dev/null +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/value_validators.test.ts @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { throwIfAbsent, throwIfIsntContained } from './value_validators'; +import uuid from 'uuid'; + +describe('throwIfAbsent', () => { + test('throws if value is absent', () => { + [undefined, null].forEach(val => { + expect(() => { + throwIfAbsent('OMG no value')(val); + }).toThrowErrorMatchingInlineSnapshot(`"OMG no value"`); + }); + }); + + test('doesnt throws if value is present but falsey', () => { + [false, ''].forEach(val => { + expect(throwIfAbsent('OMG no value')(val)).toEqual(val); + }); + }); + + test('doesnt throw if value is present', () => { + expect(throwIfAbsent('OMG no value')({})).toEqual({}); + }); +}); + +describe('throwIfIsntContained', () => { + test('throws if value is absent', () => { + expect(() => { + throwIfIsntContained(new Set([uuid.v4()]), 'OMG no value', val => val)([uuid.v4()]); + }).toThrowErrorMatchingInlineSnapshot(`"OMG no value"`); + }); + + test('throws if value is absent using custom message', () => { + const id = uuid.v4(); + expect(() => { + throwIfIsntContained( + new Set([id]), + (value: string) => `OMG no ${value}`, + val => val + )([uuid.v4()]); + }).toThrow(`OMG no ${id}`); + }); + + test('returns values if value is present', () => { + const id = uuid.v4(); + const values = [uuid.v4(), uuid.v4(), id, uuid.v4()]; + expect(throwIfIsntContained(new Set([id]), 'OMG no value', val => val)(values)).toEqual( + values + ); + }); + + test('returns values if multiple values is present', () => { + const [firstId, secondId] = [uuid.v4(), uuid.v4()]; + const values = [uuid.v4(), uuid.v4(), secondId, uuid.v4(), firstId]; + expect( + throwIfIsntContained(new Set([firstId, secondId]), 'OMG no value', val => val)(values) + ).toEqual(values); + }); + + test('allows a custom value extractor', () => { + const [firstId, secondId] = [uuid.v4(), uuid.v4()]; + const values = [ + { id: firstId, some: 'prop' }, + { id: secondId, someOther: 'prop' }, + ]; + expect( + throwIfIsntContained<{ id: string }>( + new Set([firstId, secondId]), + 'OMG no value', + (val: { id: string }) => val.id + )(values) + ).toEqual(values); + }); +}); diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/value_validators.ts b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/value_validators.ts new file mode 100644 index 00000000000000..7ee73590864065 --- /dev/null +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/value_validators.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { constant } from 'lodash'; + +export function throwIfAbsent(message: string) { + return (value: T | undefined): T => { + if (value === undefined || value === null) { + throw new Error(message); + } + return value; + }; +} + +export function throwIfIsntContained( + requiredValues: Set, + message: string | ((requiredValue: string) => string), + valueExtractor: (value: T) => string +) { + const toError = typeof message === 'function' ? message : constant(message); + return (values: T[]) => { + const availableValues = new Set(values.map(valueExtractor)); + for (const value of requiredValues.values()) { + if (!availableValues.has(value)) { + throw new Error(toError(value)); + } + } + return values; + }; +} diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/action_connector_form/action_connector_form.test.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/action_connector_form/action_connector_form.test.tsx index 6896ac954bb068..f27f7d8c3054df 100644 --- a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/action_connector_form/action_connector_form.test.tsx +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/action_connector_form/action_connector_form.test.tsx @@ -82,7 +82,11 @@ describe('action_connector_form', () => { editFlyoutVisible: false, setEditFlyoutVisibility: () => {}, actionTypesIndex: { - 'my-action-type': { id: 'my-action-type', name: 'my-action-type-name' }, + 'my-action-type': { + id: 'my-action-type', + name: 'my-action-type-name', + enabled: true, + }, }, reloadConnectors: () => { return new Promise(() => {}); diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/action_connector_form/action_type_menu.test.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/action_connector_form/action_type_menu.test.tsx index 6ef2f62315d9a4..6d98a5e3d120f4 100644 --- a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/action_connector_form/action_type_menu.test.tsx +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/action_connector_form/action_type_menu.test.tsx @@ -75,8 +75,8 @@ describe('connector_add_flyout', () => { editFlyoutVisible: false, setEditFlyoutVisibility: state => {}, actionTypesIndex: { - 'first-action-type': { id: 'first-action-type', name: 'first' }, - 'second-action-type': { id: 'second-action-type', name: 'second' }, + 'first-action-type': { id: 'first-action-type', name: 'first', enabled: true }, + 'second-action-type': { id: 'second-action-type', name: 'second', enabled: true }, }, reloadConnectors: () => { return new Promise(() => {}); diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/action_connector_form/connector_add_flyout.test.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/action_connector_form/connector_add_flyout.test.tsx index 71ba52f047d617..a03296c7c36792 100644 --- a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/action_connector_form/connector_add_flyout.test.tsx +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/action_connector_form/connector_add_flyout.test.tsx @@ -58,7 +58,9 @@ describe('connector_add_flyout', () => { setAddFlyoutVisibility: state => {}, editFlyoutVisible: false, setEditFlyoutVisibility: state => {}, - actionTypesIndex: { 'my-action-type': { id: 'my-action-type', name: 'test' } }, + actionTypesIndex: { + 'my-action-type': { id: 'my-action-type', name: 'test', enabled: true }, + }, reloadConnectors: () => { return new Promise(() => {}); }, diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/action_connector_form/connector_edit_flyout.test.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/action_connector_form/connector_edit_flyout.test.tsx index 57e950a98eb2ae..0dc38523bfab83 100644 --- a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/action_connector_form/connector_edit_flyout.test.tsx +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/action_connector_form/connector_edit_flyout.test.tsx @@ -84,7 +84,7 @@ describe('connector_edit_flyout', () => { editFlyoutVisible: true, setEditFlyoutVisibility: state => {}, actionTypesIndex: { - 'test-action-type-id': { id: 'test-action-type-id', name: 'test' }, + 'test-action-type-id': { id: 'test-action-type-id', name: 'test', enabled: true }, }, reloadConnectors: () => { return new Promise(() => {}); diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alert_details/components/alert_details.test.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alert_details/components/alert_details.test.tsx new file mode 100644 index 00000000000000..228bceb87cad7d --- /dev/null +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alert_details/components/alert_details.test.tsx @@ -0,0 +1,519 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import * as React from 'react'; +import uuid from 'uuid'; +import { shallow } from 'enzyme'; +import { AlertDetails } from './alert_details'; +import { Alert, ActionType } from '../../../../types'; +import { EuiTitle, EuiBadge, EuiFlexItem, EuiButtonEmpty, EuiSwitch } from '@elastic/eui'; +import { times, random } from 'lodash'; +import { FormattedMessage } from '@kbn/i18n/react'; + +jest.mock('../../../app_context', () => ({ + useAppDependencies: jest.fn(() => ({ + http: jest.fn(), + legacy: { + capabilities: { + get: jest.fn(() => ({})), + }, + }, + })), +})); + +jest.mock('../../../lib/capabilities', () => ({ + hasSaveAlertsCapability: jest.fn(() => true), +})); + +const mockAlertApis = { + muteAlert: jest.fn(), + unmuteAlert: jest.fn(), + enableAlert: jest.fn(), + disableAlert: jest.fn(), +}; + +// const AlertDetails = withBulkAlertOperations(RawAlertDetails); +describe('alert_details', () => { + // mock Api handlers + + it('renders the alert name as a title', () => { + const alert = mockAlert(); + const alertType = { + id: '.noop', + name: 'No Op', + }; + + expect( + shallow( + + ).containsMatchingElement( + +

{alert.name}

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

{alert.name}

+
+
+ + + + + + + + + + + + + + + + + + + +
+ + + + + + {alertType.name} + + {firstAction && ( + + + {actionTypesByTypeId[firstAction.actionTypeId].name ?? + firstAction.actionTypeId} + + + )} + {otherActions.length ? ( + + +{otherActions.length} + + ) : null} + + + + + + { + if (isEnabled) { + setIsEnabled(false); + await disableAlert(alert); + } else { + setIsEnabled(true); + await enableAlert(alert); + } + }} + label={ + + } + /> + + + { + if (isMuted) { + setIsMuted(false); + await unmuteAlert(alert); + } else { + setIsMuted(true); + await muteAlert(alert); + } + }} + label={ + + } + /> + + + + + +
+
+
+ ); +}; + +export const AlertDetailsWithApi = withBulkAlertOperations(AlertDetails); diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alert_details/components/alert_details_route.test.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alert_details/components/alert_details_route.test.tsx new file mode 100644 index 00000000000000..7a40104e97d9fd --- /dev/null +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alert_details/components/alert_details_route.test.tsx @@ -0,0 +1,409 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import * as React from 'react'; +import uuid from 'uuid'; +import { shallow } from 'enzyme'; +import { createMemoryHistory, createLocation } from 'history'; +import { ToastsApi } from 'kibana/public'; +import { AlertDetailsRoute, getAlertData } from './alert_details_route'; +import { Alert } from '../../../../types'; +import { EuiLoadingSpinner } from '@elastic/eui'; + +jest.mock('../../../app_context', () => { + const toastNotifications = jest.fn(); + return { + useAppDependencies: jest.fn(() => ({ toastNotifications })), + }; +}); +describe('alert_details_route', () => { + it('render a loader while fetching data', () => { + const alert = mockAlert(); + + expect( + shallow( + + ).containsMatchingElement() + ).toBeTruthy(); + }); +}); + +describe('getAlertData useEffect handler', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('fetches alert', async () => { + const alert = mockAlert(); + const { loadAlert, loadAlertTypes, loadActionTypes } = mockApis(); + const { setAlert, setAlertType, setActionTypes } = mockStateSetter(); + + loadAlert.mockImplementationOnce(async () => alert); + + const toastNotifications = ({ + addDanger: jest.fn(), + } as unknown) as ToastsApi; + + await getAlertData( + alert.id, + loadAlert, + loadAlertTypes, + loadActionTypes, + setAlert, + setAlertType, + setActionTypes, + toastNotifications + ); + + expect(loadAlert).toHaveBeenCalledWith(alert.id); + expect(setAlert).toHaveBeenCalledWith(alert); + }); + + it('fetches alert and action types', async () => { + const actionType = { + id: '.server-log', + name: 'Server log', + enabled: true, + }; + const alert = mockAlert({ + actions: [ + { + group: '', + id: uuid.v4(), + actionTypeId: actionType.id, + params: {}, + }, + ], + }); + const alertType = { + id: alert.alertTypeId, + name: 'type name', + }; + const { loadAlert, loadAlertTypes, loadActionTypes } = mockApis(); + const { setAlert, setAlertType, setActionTypes } = mockStateSetter(); + + loadAlert.mockImplementation(async () => alert); + loadAlertTypes.mockImplementation(async () => [alertType]); + loadActionTypes.mockImplementation(async () => [actionType]); + + const toastNotifications = ({ + addDanger: jest.fn(), + } as unknown) as ToastsApi; + + await getAlertData( + alert.id, + loadAlert, + loadAlertTypes, + loadActionTypes, + setAlert, + setAlertType, + setActionTypes, + toastNotifications + ); + + expect(loadAlertTypes).toHaveBeenCalledTimes(1); + expect(loadActionTypes).toHaveBeenCalledTimes(1); + + expect(setAlertType).toHaveBeenCalledWith(alertType); + expect(setActionTypes).toHaveBeenCalledWith([actionType]); + }); + + it('displays an error if the alert isnt found', async () => { + const actionType = { + id: '.server-log', + name: 'Server log', + enabled: true, + }; + const alert = mockAlert({ + actions: [ + { + group: '', + id: uuid.v4(), + actionTypeId: actionType.id, + params: {}, + }, + ], + }); + + const { loadAlert, loadAlertTypes, loadActionTypes } = mockApis(); + const { setAlert, setAlertType, setActionTypes } = mockStateSetter(); + + loadAlert.mockImplementation(async () => { + throw new Error('OMG'); + }); + + const toastNotifications = ({ + addDanger: jest.fn(), + } as unknown) as ToastsApi; + await getAlertData( + alert.id, + loadAlert, + loadAlertTypes, + loadActionTypes, + setAlert, + setAlertType, + setActionTypes, + toastNotifications + ); + expect(toastNotifications.addDanger).toHaveBeenCalledTimes(1); + expect(toastNotifications.addDanger).toHaveBeenCalledWith({ + title: 'Unable to load alert: OMG', + }); + }); + + it('displays an error if the alert type isnt loaded', async () => { + const actionType = { + id: '.server-log', + name: 'Server log', + enabled: true, + }; + const alert = mockAlert({ + actions: [ + { + group: '', + id: uuid.v4(), + actionTypeId: actionType.id, + params: {}, + }, + ], + }); + + const { loadAlert, loadAlertTypes, loadActionTypes } = mockApis(); + const { setAlert, setAlertType, setActionTypes } = mockStateSetter(); + + loadAlert.mockImplementation(async () => alert); + + loadAlertTypes.mockImplementation(async () => { + throw new Error('OMG no alert type'); + }); + loadActionTypes.mockImplementation(async () => [actionType]); + + const toastNotifications = ({ + addDanger: jest.fn(), + } as unknown) as ToastsApi; + await getAlertData( + alert.id, + loadAlert, + loadAlertTypes, + loadActionTypes, + setAlert, + setAlertType, + setActionTypes, + toastNotifications + ); + expect(toastNotifications.addDanger).toHaveBeenCalledTimes(1); + expect(toastNotifications.addDanger).toHaveBeenCalledWith({ + title: 'Unable to load alert: OMG no alert type', + }); + }); + + it('displays an error if the action type isnt loaded', async () => { + const actionType = { + id: '.server-log', + name: 'Server log', + enabled: true, + }; + const alert = mockAlert({ + actions: [ + { + group: '', + id: uuid.v4(), + actionTypeId: actionType.id, + params: {}, + }, + ], + }); + const alertType = { + id: alert.alertTypeId, + name: 'type name', + }; + + const { loadAlert, loadAlertTypes, loadActionTypes } = mockApis(); + const { setAlert, setAlertType, setActionTypes } = mockStateSetter(); + + loadAlert.mockImplementation(async () => alert); + + loadAlertTypes.mockImplementation(async () => [alertType]); + loadActionTypes.mockImplementation(async () => { + throw new Error('OMG no action type'); + }); + + const toastNotifications = ({ + addDanger: jest.fn(), + } as unknown) as ToastsApi; + await getAlertData( + alert.id, + loadAlert, + loadAlertTypes, + loadActionTypes, + setAlert, + setAlertType, + setActionTypes, + toastNotifications + ); + expect(toastNotifications.addDanger).toHaveBeenCalledTimes(1); + expect(toastNotifications.addDanger).toHaveBeenCalledWith({ + title: 'Unable to load alert: OMG no action type', + }); + }); + + it('displays an error if the alert type isnt found', async () => { + const actionType = { + id: '.server-log', + name: 'Server log', + enabled: true, + }; + const alert = mockAlert({ + actions: [ + { + group: '', + id: uuid.v4(), + actionTypeId: actionType.id, + params: {}, + }, + ], + }); + + const alertType = { + id: uuid.v4(), + name: 'type name', + }; + + const { loadAlert, loadAlertTypes, loadActionTypes } = mockApis(); + const { setAlert, setAlertType, setActionTypes } = mockStateSetter(); + + loadAlert.mockImplementation(async () => alert); + loadAlertTypes.mockImplementation(async () => [alertType]); + loadActionTypes.mockImplementation(async () => [actionType]); + + const toastNotifications = ({ + addDanger: jest.fn(), + } as unknown) as ToastsApi; + await getAlertData( + alert.id, + loadAlert, + loadAlertTypes, + loadActionTypes, + setAlert, + setAlertType, + setActionTypes, + toastNotifications + ); + expect(toastNotifications.addDanger).toHaveBeenCalledTimes(1); + expect(toastNotifications.addDanger).toHaveBeenCalledWith({ + title: `Unable to load alert: Invalid Alert Type: ${alert.alertTypeId}`, + }); + }); + + it('displays an error if an action type isnt found', async () => { + const availableActionType = { + id: '.server-log', + name: 'Server log', + enabled: true, + }; + const missingActionType = { + id: '.noop', + name: 'No Op', + enabled: true, + }; + const alert = mockAlert({ + actions: [ + { + group: '', + id: uuid.v4(), + actionTypeId: availableActionType.id, + params: {}, + }, + { + group: '', + id: uuid.v4(), + actionTypeId: missingActionType.id, + params: {}, + }, + ], + }); + + const alertType = { + id: uuid.v4(), + name: 'type name', + }; + + const { loadAlert, loadAlertTypes, loadActionTypes } = mockApis(); + const { setAlert, setAlertType, setActionTypes } = mockStateSetter(); + + loadAlert.mockImplementation(async () => alert); + loadAlertTypes.mockImplementation(async () => [alertType]); + loadActionTypes.mockImplementation(async () => [availableActionType]); + + const toastNotifications = ({ + addDanger: jest.fn(), + } as unknown) as ToastsApi; + await getAlertData( + alert.id, + loadAlert, + loadAlertTypes, + loadActionTypes, + setAlert, + setAlertType, + setActionTypes, + toastNotifications + ); + expect(toastNotifications.addDanger).toHaveBeenCalledTimes(1); + expect(toastNotifications.addDanger).toHaveBeenCalledWith({ + title: `Unable to load alert: Invalid Action Type: ${missingActionType.id}`, + }); + }); +}); + +function mockApis() { + return { + loadAlert: jest.fn(), + loadAlertTypes: jest.fn(), + loadActionTypes: jest.fn(), + }; +} + +function mockStateSetter() { + return { + setAlert: jest.fn(), + setAlertType: jest.fn(), + setActionTypes: jest.fn(), + }; +} + +function mockRouterProps(alert: Alert) { + return { + match: { + isExact: false, + path: `/alert/${alert.id}`, + url: '', + params: { alertId: alert.id }, + }, + history: createMemoryHistory(), + location: createLocation(`/alert/${alert.id}`), + }; +} +function mockAlert(overloads: Partial = {}): Alert { + return { + id: uuid.v4(), + enabled: true, + name: `alert-${uuid.v4()}`, + tags: [], + alertTypeId: '.noop', + consumer: 'consumer', + schedule: { interval: '1m' }, + actions: [], + params: {}, + createdBy: null, + updatedBy: null, + createdAt: new Date(), + updatedAt: new Date(), + apiKeyOwner: null, + throttle: null, + muteAll: false, + mutedInstanceIds: [], + ...overloads, + }; +} diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alert_details/components/alert_details_route.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alert_details/components/alert_details_route.tsx new file mode 100644 index 00000000000000..4e00ea304d987d --- /dev/null +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alert_details/components/alert_details_route.tsx @@ -0,0 +1,118 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import React, { useState, useEffect } from 'react'; +import { RouteComponentProps } from 'react-router-dom'; +import { EuiLoadingSpinner } from '@elastic/eui'; +import { ToastsApi } from 'kibana/public'; +import { Alert, AlertType, ActionType } from '../../../../types'; +import { useAppDependencies } from '../../../app_context'; +import { AlertDetailsWithApi as AlertDetails } from './alert_details'; +import { throwIfAbsent, throwIfIsntContained } from '../../../lib/value_validators'; +import { + ComponentOpts as AlertApis, + withBulkAlertOperations, +} from '../../common/components/with_bulk_alert_api_operations'; +import { + ComponentOpts as ActionApis, + withActionOperations, +} from '../../common/components/with_actions_api_operations'; + +type AlertDetailsRouteProps = RouteComponentProps<{ + alertId: string; +}> & + Pick & + Pick; + +export const AlertDetailsRoute: React.FunctionComponent = ({ + match: { + params: { alertId }, + }, + loadAlert, + loadAlertTypes, + loadActionTypes, +}) => { + const { http, toastNotifications } = useAppDependencies(); + + const [alert, setAlert] = useState(null); + const [alertType, setAlertType] = useState(null); + const [actionTypes, setActionTypes] = useState(null); + + useEffect(() => { + getAlertData( + alertId, + loadAlert, + loadAlertTypes, + loadActionTypes, + setAlert, + setAlertType, + setActionTypes, + toastNotifications + ); + }, [alertId, http, loadActionTypes, loadAlert, loadAlertTypes, toastNotifications]); + + return alert && alertType && actionTypes ? ( + + ) : ( +
+ +
+ ); +}; + +export async function getAlertData( + alertId: string, + loadAlert: AlertApis['loadAlert'], + loadAlertTypes: AlertApis['loadAlertTypes'], + loadActionTypes: ActionApis['loadActionTypes'], + setAlert: React.Dispatch>, + setAlertType: React.Dispatch>, + setActionTypes: React.Dispatch>, + toastNotifications: Pick +) { + try { + const loadedAlert = await loadAlert(alertId); + setAlert(loadedAlert); + + const [loadedAlertType, loadedActionTypes] = await Promise.all([ + loadAlertTypes() + .then(types => types.find(type => type.id === loadedAlert.alertTypeId)) + .then(throwIfAbsent(`Invalid Alert Type: ${loadedAlert.alertTypeId}`)), + loadActionTypes().then( + throwIfIsntContained( + new Set(loadedAlert.actions.map(action => action.actionTypeId)), + (requiredActionType: string) => `Invalid Action Type: ${requiredActionType}`, + (action: ActionType) => action.id + ) + ), + ]); + + setAlertType(loadedAlertType); + setActionTypes(loadedActionTypes); + } catch (e) { + toastNotifications.addDanger({ + title: i18n.translate( + 'xpack.triggersActionsUI.sections.alertDetails.unableToLoadAlertMessage', + { + defaultMessage: 'Unable to load alert: {message}', + values: { + message: e.message, + }, + } + ), + }); + } +} + +export const AlertDetailsRouteWithApi = withActionOperations( + withBulkAlertOperations(AlertDetailsRoute) +); diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alerts_list/components/alerts_list.test.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alerts_list/components/alerts_list.test.tsx index ff1510ea873d3b..f410fff44172fa 100644 --- a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alerts_list/components/alerts_list.test.tsx +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alerts_list/components/alerts_list.test.tsx @@ -21,7 +21,14 @@ jest.mock('../../../lib/alert_api', () => ({ loadAlerts: jest.fn(), loadAlertTypes: jest.fn(), })); - +jest.mock('react-router-dom', () => ({ + useHistory: () => ({ + push: jest.fn(), + }), + useLocation: () => ({ + pathname: '/triggersActions/alerts/', + }), +})); const actionTypeRegistry = actionTypeRegistryMock.create(); const alertTypeRegistry = alertTypeRegistryMock.create(); diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alerts_list/components/alerts_list.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alerts_list/components/alerts_list.tsx index 12122983161bd8..32de924f63e807 100644 --- a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alerts_list/components/alerts_list.tsx +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alerts_list/components/alerts_list.tsx @@ -15,19 +15,23 @@ import { EuiFlexItem, EuiIcon, EuiSpacer, + EuiLink, } from '@elastic/eui'; +import { useHistory } from 'react-router-dom'; import { AlertsContextProvider } from '../../../context/alerts_context'; import { useAppDependencies } from '../../../app_context'; import { ActionType, Alert, AlertTableItem, AlertTypeIndex, Pagination } from '../../../../types'; import { AlertAdd } from '../../alert_add'; -import { BulkActionPopover } from './bulk_action_popover'; -import { CollapsedItemActions } from './collapsed_item_actions'; +import { BulkOperationPopover } from '../../common/components/bulk_operation_popover'; +import { AlertQuickEditButtonsWithApi as AlertQuickEditButtons } from '../../common/components/alert_quick_edit_buttons'; +import { CollapsedItemActionsWithApi as CollapsedItemActions } from './collapsed_item_actions'; import { TypeFilter } from './type_filter'; import { ActionTypeFilter } from './action_type_filter'; import { loadAlerts, loadAlertTypes } from '../../../lib/alert_api'; import { loadActionTypes } from '../../../lib/action_connector_api'; import { hasDeleteAlertsCapability, hasSaveAlertsCapability } from '../../../lib/capabilities'; +import { routeToAlertDetails } from '../../../constants'; const ENTER_KEY = 13; @@ -43,6 +47,7 @@ interface AlertState { } export const AlertsList: React.FunctionComponent = () => { + const history = useHistory(); const { http, injectedMetadata, toastNotifications, capabilities } = useAppDependencies(); const canDelete = hasDeleteAlertsCapability(capabilities); const canSave = hasSaveAlertsCapability(capabilities); @@ -151,6 +156,18 @@ export const AlertsList: React.FunctionComponent = () => { sortable: false, truncateText: true, 'data-test-subj': 'alertsTableCell-name', + render: (name: string, alert: AlertTableItem) => { + return ( + { + history.push(routeToAlertDetails.replace(`:alertId`, alert.id)); + }} + > + {name} + + ); + }, }, { field: 'tagsText', @@ -236,17 +253,19 @@ export const AlertsList: React.FunctionComponent = () => { {selectedIds.length > 0 && canDelete && ( - setIsPerformingAction(true)} - onActionPerformed={() => { - loadAlertsData(); - setIsPerformingAction(false); - }} - /> + + setIsPerformingAction(true)} + onActionPerformed={() => { + loadAlertsData(); + setIsPerformingAction(false); + }} + /> + )} diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alerts_list/components/collapsed_item_actions.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alerts_list/components/collapsed_item_actions.tsx index aa1c6dd7c5b9ac..2bac159ed79ede 100644 --- a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alerts_list/components/collapsed_item_actions.tsx +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alerts_list/components/collapsed_item_actions.tsx @@ -20,23 +20,25 @@ import { AlertTableItem } from '../../../../types'; import { useAppDependencies } from '../../../app_context'; import { hasDeleteAlertsCapability, hasSaveAlertsCapability } from '../../../lib/capabilities'; import { - deleteAlerts, - disableAlerts, - enableAlerts, - muteAlerts, - unmuteAlerts, -} from '../../../lib/alert_api'; + ComponentOpts as BulkOperationsComponentOpts, + withBulkAlertOperations, +} from '../../common/components/with_bulk_alert_api_operations'; -export interface ComponentOpts { +export type ComponentOpts = { item: AlertTableItem; onAlertChanged: () => void; -} +} & BulkOperationsComponentOpts; export const CollapsedItemActions: React.FunctionComponent = ({ item, onAlertChanged, + disableAlert, + enableAlert, + unmuteAlert, + muteAlert, + deleteAlert, }: ComponentOpts) => { - const { http, capabilities } = useAppDependencies(); + const { capabilities } = useAppDependencies(); const canDelete = hasDeleteAlertsCapability(capabilities); const canSave = hasSaveAlertsCapability(capabilities); @@ -71,9 +73,9 @@ export const CollapsedItemActions: React.FunctionComponent = ({ data-test-subj="enableSwitch" onChange={async () => { if (item.enabled) { - await disableAlerts({ http, ids: [item.id] }); + await disableAlert(item); } else { - await enableAlerts({ http, ids: [item.id] }); + await enableAlert(item); } onAlertChanged(); }} @@ -93,9 +95,9 @@ export const CollapsedItemActions: React.FunctionComponent = ({ data-test-subj="muteSwitch" onChange={async () => { if (item.muteAll) { - await unmuteAlerts({ http, ids: [item.id] }); + await unmuteAlert(item); } else { - await muteAlerts({ http, ids: [item.id] }); + await muteAlert(item); } onAlertChanged(); }} @@ -115,7 +117,7 @@ export const CollapsedItemActions: React.FunctionComponent = ({ color="text" data-test-subj="deleteAlert" onClick={async () => { - await deleteAlerts({ http, ids: [item.id] }); + await deleteAlert(item); onAlertChanged(); }} > @@ -129,3 +131,5 @@ export const CollapsedItemActions: React.FunctionComponent = ({
); }; + +export const CollapsedItemActionsWithApi = withBulkAlertOperations(CollapsedItemActions); diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alerts_list/components/bulk_action_popover.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/alert_quick_edit_buttons.tsx similarity index 52% rename from x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alerts_list/components/bulk_action_popover.tsx rename to x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/alert_quick_edit_buttons.tsx index 59ec52ac83a6c5..9635e6cd119836 100644 --- a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alerts_list/components/bulk_action_popover.tsx +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/alert_quick_edit_buttons.tsx @@ -5,34 +5,35 @@ */ import { i18n } from '@kbn/i18n'; -import React, { useState } from 'react'; +import React, { useState, Fragment } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiButton, EuiButtonEmpty, EuiFormRow, EuiPopover } from '@elastic/eui'; +import { EuiButtonEmpty } from '@elastic/eui'; -import { AlertTableItem } from '../../../../types'; +import { Alert } from '../../../../types'; import { useAppDependencies } from '../../../app_context'; import { - deleteAlerts, - disableAlerts, - enableAlerts, - muteAlerts, - unmuteAlerts, -} from '../../../lib/alert_api'; + withBulkAlertOperations, + ComponentOpts as BulkOperationsComponentOpts, +} from './with_bulk_alert_api_operations'; -export interface ComponentOpts { - selectedItems: AlertTableItem[]; - onPerformingAction: () => void; - onActionPerformed: () => void; -} +export type ComponentOpts = { + selectedItems: Alert[]; + onPerformingAction?: () => void; + onActionPerformed?: () => void; +} & BulkOperationsComponentOpts; -export const BulkActionPopover: React.FunctionComponent = ({ +export const AlertQuickEditButtons: React.FunctionComponent = ({ selectedItems, - onPerformingAction, - onActionPerformed, + onPerformingAction = noop, + onActionPerformed = noop, + muteAlerts, + unmuteAlerts, + enableAlerts, + disableAlerts, + deleteAlerts, }: ComponentOpts) => { - const { http, toastNotifications } = useAppDependencies(); + const { toastNotifications } = useAppDependencies(); - const [isPopoverOpen, setIsPopoverOpen] = useState(false); const [isMutingAlerts, setIsMutingAlerts] = useState(false); const [isUnmutingAlerts, setIsUnmutingAlerts] = useState(false); const [isEnablingAlerts, setIsEnablingAlerts] = useState(false); @@ -47,9 +48,8 @@ export const BulkActionPopover: React.FunctionComponent = ({ async function onmMuteAllClick() { onPerformingAction(); setIsMutingAlerts(true); - const ids = selectedItems.filter(item => !isAlertMuted(item)).map(item => item.id); try { - await muteAlerts({ http, ids }); + await muteAlerts(selectedItems); } catch (e) { toastNotifications.addDanger({ title: i18n.translate( @@ -68,9 +68,8 @@ export const BulkActionPopover: React.FunctionComponent = ({ async function onUnmuteAllClick() { onPerformingAction(); setIsUnmutingAlerts(true); - const ids = selectedItems.filter(isAlertMuted).map(item => item.id); try { - await unmuteAlerts({ http, ids }); + await unmuteAlerts(selectedItems); } catch (e) { toastNotifications.addDanger({ title: i18n.translate( @@ -89,9 +88,8 @@ export const BulkActionPopover: React.FunctionComponent = ({ async function onEnableAllClick() { onPerformingAction(); setIsEnablingAlerts(true); - const ids = selectedItems.filter(isAlertDisabled).map(item => item.id); try { - await enableAlerts({ http, ids }); + await enableAlerts(selectedItems); } catch (e) { toastNotifications.addDanger({ title: i18n.translate( @@ -110,9 +108,8 @@ export const BulkActionPopover: React.FunctionComponent = ({ async function onDisableAllClick() { onPerformingAction(); setIsDisablingAlerts(true); - const ids = selectedItems.filter(item => !isAlertDisabled(item)).map(item => item.id); try { - await disableAlerts({ http, ids }); + await disableAlerts(selectedItems); } catch (e) { toastNotifications.addDanger({ title: i18n.translate( @@ -131,9 +128,8 @@ export const BulkActionPopover: React.FunctionComponent = ({ async function deleteSelectedItems() { onPerformingAction(); setIsDeletingAlerts(true); - const ids = selectedItems.map(item => item.id); try { - await deleteAlerts({ http, ids }); + await deleteAlerts(selectedItems); } catch (e) { toastNotifications.addDanger({ title: i18n.translate( @@ -150,104 +146,83 @@ export const BulkActionPopover: React.FunctionComponent = ({ } return ( - setIsPopoverOpen(false)} - data-test-subj="bulkAction" - button={ - setIsPopoverOpen(!isPopoverOpen)} + + {!allAlertsMuted && ( + - - } - > - {!allAlertsMuted && ( - - - - - + )} {allAlertsMuted && ( - - - - - + + + )} {allAlertsDisabled && ( - - - - - + + + )} {!allAlertsDisabled && ( - - - - - - )} - - - + )} + + + + + ); }; -function isAlertDisabled(alert: AlertTableItem) { +export const AlertQuickEditButtonsWithApi = withBulkAlertOperations(AlertQuickEditButtons); + +function isAlertDisabled(alert: Alert) { return alert.enabled === false; } -function isAlertMuted(alert: AlertTableItem) { +function isAlertMuted(alert: Alert) { return alert.muteAll === true; } + +function noop() {} diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/bulk_operation_popover.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/bulk_operation_popover.tsx new file mode 100644 index 00000000000000..d0fd0e17928183 --- /dev/null +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/bulk_operation_popover.tsx @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiButton, EuiFormRow, EuiPopover } from '@elastic/eui'; + +export const BulkOperationPopover: React.FunctionComponent = ({ children }) => { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + return ( + setIsPopoverOpen(false)} + data-test-subj="bulkAction" + button={ + setIsPopoverOpen(!isPopoverOpen)} + > + + + } + > + {children && + React.Children.map(children, child => + React.isValidElement(child) ? ( + {React.cloneElement(child, {})} + ) : ( + child + ) + )} + + ); +}; diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/with_actions_api_operations.test.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/with_actions_api_operations.test.tsx new file mode 100644 index 00000000000000..dd6b8775ba3d09 --- /dev/null +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/with_actions_api_operations.test.tsx @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import * as React from 'react'; +import { shallow, mount } from 'enzyme'; +import { withActionOperations, ComponentOpts } from './with_actions_api_operations'; +import * as actionApis from '../../../lib/action_connector_api'; +import { useAppDependencies } from '../../../app_context'; + +jest.mock('../../../lib/action_connector_api'); + +jest.mock('../../../app_context', () => { + const http = jest.fn(); + return { + useAppDependencies: jest.fn(() => ({ + http, + })), + }; +}); + +describe('with_action_api_operations', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('extends any component with Action Api methods', () => { + const ComponentToExtend = (props: ComponentOpts) => { + expect(typeof props.loadActionTypes).toEqual('function'); + return
; + }; + + const ExtendedComponent = withActionOperations(ComponentToExtend); + expect(shallow().type()).toEqual(ComponentToExtend); + }); + + it('loadActionTypes calls the loadActionTypes api', () => { + const { http } = useAppDependencies(); + const ComponentToExtend = ({ loadActionTypes }: ComponentOpts) => { + return ; + }; + + const ExtendedComponent = withActionOperations(ComponentToExtend); + const component = mount(); + component.find('button').simulate('click'); + + expect(actionApis.loadActionTypes).toHaveBeenCalledTimes(1); + expect(actionApis.loadActionTypes).toHaveBeenCalledWith({ http }); + }); +}); diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/with_actions_api_operations.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/with_actions_api_operations.tsx new file mode 100644 index 00000000000000..45e6c6b10532ce --- /dev/null +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/with_actions_api_operations.tsx @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { ActionType } from '../../../../types'; +import { useAppDependencies } from '../../../app_context'; +import { loadActionTypes } from '../../../lib/action_connector_api'; + +export interface ComponentOpts { + loadActionTypes: () => Promise; +} + +export type PropsWithOptionalApiHandlers = Omit & Partial; + +export function withActionOperations( + WrappedComponent: React.ComponentType +): React.FunctionComponent> { + return (props: PropsWithOptionalApiHandlers) => { + const { http } = useAppDependencies(); + return ( + loadActionTypes({ http })} /> + ); + }; +} diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/with_bulk_alert_api_operations.test.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/with_bulk_alert_api_operations.test.tsx new file mode 100644 index 00000000000000..30a065479ce33c --- /dev/null +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/with_bulk_alert_api_operations.test.tsx @@ -0,0 +1,269 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import * as React from 'react'; +import { shallow, mount } from 'enzyme'; +import uuid from 'uuid'; +import { withBulkAlertOperations, ComponentOpts } from './with_bulk_alert_api_operations'; +import * as alertApi from '../../../lib/alert_api'; +import { useAppDependencies } from '../../../app_context'; +import { Alert } from '../../../../types'; + +jest.mock('../../../lib/alert_api'); + +jest.mock('../../../app_context', () => { + const http = jest.fn(); + return { + useAppDependencies: jest.fn(() => ({ + http, + legacy: { + capabilities: { + get: jest.fn(() => ({})), + }, + }, + })), + }; +}); + +describe('with_bulk_alert_api_operations', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('extends any component with AlertApi methods', () => { + const ComponentToExtend = (props: ComponentOpts) => { + expect(typeof props.muteAlerts).toEqual('function'); + expect(typeof props.unmuteAlerts).toEqual('function'); + expect(typeof props.enableAlerts).toEqual('function'); + expect(typeof props.disableAlerts).toEqual('function'); + expect(typeof props.deleteAlerts).toEqual('function'); + expect(typeof props.muteAlert).toEqual('function'); + expect(typeof props.unmuteAlert).toEqual('function'); + expect(typeof props.enableAlert).toEqual('function'); + expect(typeof props.disableAlert).toEqual('function'); + expect(typeof props.deleteAlert).toEqual('function'); + expect(typeof props.loadAlert).toEqual('function'); + expect(typeof props.loadAlertTypes).toEqual('function'); + return
; + }; + + const ExtendedComponent = withBulkAlertOperations(ComponentToExtend); + expect(shallow().type()).toEqual(ComponentToExtend); + }); + + // single alert + it('muteAlert calls the muteAlert api', () => { + const { http } = useAppDependencies(); + const ComponentToExtend = ({ muteAlert, alert }: ComponentOpts & { alert: Alert }) => { + return ; + }; + + const ExtendedComponent = withBulkAlertOperations(ComponentToExtend); + const alert = mockAlert(); + const component = mount(); + component.find('button').simulate('click'); + + expect(alertApi.muteAlert).toHaveBeenCalledTimes(1); + expect(alertApi.muteAlert).toHaveBeenCalledWith({ id: alert.id, http }); + }); + + it('unmuteAlert calls the unmuteAlert api', () => { + const { http } = useAppDependencies(); + const ComponentToExtend = ({ unmuteAlert, alert }: ComponentOpts & { alert: Alert }) => { + return ; + }; + + const ExtendedComponent = withBulkAlertOperations(ComponentToExtend); + const alert = mockAlert({ muteAll: true }); + const component = mount(); + component.find('button').simulate('click'); + + expect(alertApi.unmuteAlert).toHaveBeenCalledTimes(1); + expect(alertApi.unmuteAlert).toHaveBeenCalledWith({ id: alert.id, http }); + }); + + it('enableAlert calls the muteAlerts api', () => { + const { http } = useAppDependencies(); + const ComponentToExtend = ({ enableAlert, alert }: ComponentOpts & { alert: Alert }) => { + return ; + }; + + const ExtendedComponent = withBulkAlertOperations(ComponentToExtend); + const alert = mockAlert({ enabled: false }); + const component = mount(); + component.find('button').simulate('click'); + + expect(alertApi.enableAlert).toHaveBeenCalledTimes(1); + expect(alertApi.enableAlert).toHaveBeenCalledWith({ id: alert.id, http }); + }); + + it('disableAlert calls the disableAlert api', () => { + const { http } = useAppDependencies(); + const ComponentToExtend = ({ disableAlert, alert }: ComponentOpts & { alert: Alert }) => { + return ; + }; + + const ExtendedComponent = withBulkAlertOperations(ComponentToExtend); + const alert = mockAlert(); + const component = mount(); + component.find('button').simulate('click'); + + expect(alertApi.disableAlert).toHaveBeenCalledTimes(1); + expect(alertApi.disableAlert).toHaveBeenCalledWith({ id: alert.id, http }); + }); + + it('deleteAlert calls the deleteAlert api', () => { + const { http } = useAppDependencies(); + const ComponentToExtend = ({ deleteAlert, alert }: ComponentOpts & { alert: Alert }) => { + return ; + }; + + const ExtendedComponent = withBulkAlertOperations(ComponentToExtend); + const alert = mockAlert(); + const component = mount(); + component.find('button').simulate('click'); + + expect(alertApi.deleteAlert).toHaveBeenCalledTimes(1); + expect(alertApi.deleteAlert).toHaveBeenCalledWith({ id: alert.id, http }); + }); + + // bulk alerts + it('muteAlerts calls the muteAlerts api', () => { + const { http } = useAppDependencies(); + const ComponentToExtend = ({ muteAlerts, alerts }: ComponentOpts & { alerts: Alert[] }) => { + return ; + }; + + const ExtendedComponent = withBulkAlertOperations(ComponentToExtend); + const alerts = [mockAlert(), mockAlert()]; + const component = mount(); + component.find('button').simulate('click'); + + expect(alertApi.muteAlerts).toHaveBeenCalledTimes(1); + expect(alertApi.muteAlerts).toHaveBeenCalledWith({ ids: [alerts[0].id, alerts[1].id], http }); + }); + + it('unmuteAlerts calls the unmuteAlerts api', () => { + const { http } = useAppDependencies(); + const ComponentToExtend = ({ unmuteAlerts, alerts }: ComponentOpts & { alerts: Alert[] }) => { + return ; + }; + + const ExtendedComponent = withBulkAlertOperations(ComponentToExtend); + const alerts = [mockAlert({ muteAll: true }), mockAlert({ muteAll: true })]; + const component = mount(); + component.find('button').simulate('click'); + + expect(alertApi.unmuteAlerts).toHaveBeenCalledTimes(1); + expect(alertApi.unmuteAlerts).toHaveBeenCalledWith({ ids: [alerts[0].id, alerts[1].id], http }); + }); + + it('enableAlerts calls the muteAlertss api', () => { + const { http } = useAppDependencies(); + const ComponentToExtend = ({ enableAlerts, alerts }: ComponentOpts & { alerts: Alert[] }) => { + return ; + }; + + const ExtendedComponent = withBulkAlertOperations(ComponentToExtend); + const alerts = [ + mockAlert({ enabled: false }), + mockAlert({ enabled: true }), + mockAlert({ enabled: false }), + ]; + const component = mount(); + component.find('button').simulate('click'); + + expect(alertApi.enableAlerts).toHaveBeenCalledTimes(1); + expect(alertApi.enableAlerts).toHaveBeenCalledWith({ ids: [alerts[0].id, alerts[2].id], http }); + }); + + it('disableAlerts calls the disableAlerts api', () => { + const { http } = useAppDependencies(); + const ComponentToExtend = ({ disableAlerts, alerts }: ComponentOpts & { alerts: Alert[] }) => { + return ; + }; + + const ExtendedComponent = withBulkAlertOperations(ComponentToExtend); + const alerts = [mockAlert(), mockAlert()]; + const component = mount(); + component.find('button').simulate('click'); + + expect(alertApi.disableAlerts).toHaveBeenCalledTimes(1); + expect(alertApi.disableAlerts).toHaveBeenCalledWith({ + ids: [alerts[0].id, alerts[1].id], + http, + }); + }); + + it('deleteAlerts calls the deleteAlerts api', () => { + const { http } = useAppDependencies(); + const ComponentToExtend = ({ deleteAlerts, alerts }: ComponentOpts & { alerts: Alert[] }) => { + return ; + }; + + const ExtendedComponent = withBulkAlertOperations(ComponentToExtend); + const alerts = [mockAlert(), mockAlert()]; + const component = mount(); + component.find('button').simulate('click'); + + expect(alertApi.deleteAlerts).toHaveBeenCalledTimes(1); + expect(alertApi.deleteAlerts).toHaveBeenCalledWith({ ids: [alerts[0].id, alerts[1].id], http }); + }); + + it('loadAlert calls the loadAlert api', () => { + const { http } = useAppDependencies(); + const ComponentToExtend = ({ + loadAlert, + alertId, + }: ComponentOpts & { alertId: Alert['id'] }) => { + return ; + }; + + const ExtendedComponent = withBulkAlertOperations(ComponentToExtend); + const alertId = uuid.v4(); + const component = mount(); + component.find('button').simulate('click'); + + expect(alertApi.loadAlert).toHaveBeenCalledTimes(1); + expect(alertApi.loadAlert).toHaveBeenCalledWith({ alertId, http }); + }); + + it('loadAlertTypes calls the loadAlertTypes api', () => { + const { http } = useAppDependencies(); + const ComponentToExtend = ({ loadAlertTypes }: ComponentOpts) => { + return ; + }; + + const ExtendedComponent = withBulkAlertOperations(ComponentToExtend); + const component = mount(); + component.find('button').simulate('click'); + + expect(alertApi.loadAlertTypes).toHaveBeenCalledTimes(1); + expect(alertApi.loadAlertTypes).toHaveBeenCalledWith({ http }); + }); +}); + +function mockAlert(overloads: Partial = {}): Alert { + return { + id: uuid.v4(), + enabled: true, + name: `alert-${uuid.v4()}`, + tags: [], + alertTypeId: '.noop', + consumer: 'consumer', + schedule: { interval: '1m' }, + actions: [], + params: {}, + createdBy: null, + updatedBy: null, + createdAt: new Date(), + updatedAt: new Date(), + apiKeyOwner: null, + throttle: null, + muteAll: false, + mutedInstanceIds: [], + ...overloads, + }; +} diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/with_bulk_alert_api_operations.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/with_bulk_alert_api_operations.tsx new file mode 100644 index 00000000000000..c61ba631ab8685 --- /dev/null +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/with_bulk_alert_api_operations.tsx @@ -0,0 +1,103 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { Alert, AlertType } from '../../../../types'; +import { useAppDependencies } from '../../../app_context'; +import { + deleteAlerts, + disableAlerts, + enableAlerts, + muteAlerts, + unmuteAlerts, + deleteAlert, + disableAlert, + enableAlert, + muteAlert, + unmuteAlert, + loadAlert, + loadAlertTypes, +} from '../../../lib/alert_api'; + +export interface ComponentOpts { + muteAlerts: (alerts: Alert[]) => Promise; + unmuteAlerts: (alerts: Alert[]) => Promise; + enableAlerts: (alerts: Alert[]) => Promise; + disableAlerts: (alerts: Alert[]) => Promise; + deleteAlerts: (alerts: Alert[]) => Promise; + muteAlert: (alert: Alert) => Promise; + unmuteAlert: (alert: Alert) => Promise; + enableAlert: (alert: Alert) => Promise; + disableAlert: (alert: Alert) => Promise; + deleteAlert: (alert: Alert) => Promise; + loadAlert: (id: Alert['id']) => Promise; + loadAlertTypes: () => Promise; +} + +export type PropsWithOptionalApiHandlers = Omit & Partial; + +export function withBulkAlertOperations( + WrappedComponent: React.ComponentType +): React.FunctionComponent> { + return (props: PropsWithOptionalApiHandlers) => { + const { http } = useAppDependencies(); + return ( + + muteAlerts({ http, ids: items.filter(item => !isAlertMuted(item)).map(item => item.id) }) + } + unmuteAlerts={async (items: Alert[]) => + unmuteAlerts({ http, ids: items.filter(isAlertMuted).map(item => item.id) }) + } + enableAlerts={async (items: Alert[]) => + enableAlerts({ http, ids: items.filter(isAlertDisabled).map(item => item.id) }) + } + disableAlerts={async (items: Alert[]) => + disableAlerts({ + http, + ids: items.filter(item => !isAlertDisabled(item)).map(item => item.id), + }) + } + deleteAlerts={async (items: Alert[]) => + deleteAlerts({ http, ids: items.map(item => item.id) }) + } + muteAlert={async (alert: Alert) => { + if (!isAlertMuted(alert)) { + return muteAlert({ http, id: alert.id }); + } + }} + unmuteAlert={async (alert: Alert) => { + if (isAlertMuted(alert)) { + return unmuteAlert({ http, id: alert.id }); + } + }} + enableAlert={async (alert: Alert) => { + if (isAlertDisabled(alert)) { + return enableAlert({ http, id: alert.id }); + } + }} + disableAlert={async (alert: Alert) => { + if (!isAlertDisabled(alert)) { + return disableAlert({ http, id: alert.id }); + } + }} + deleteAlert={async (alert: Alert) => deleteAlert({ http, id: alert.id })} + loadAlert={async (alertId: Alert['id']) => loadAlert({ http, alertId })} + loadAlertTypes={async () => loadAlertTypes({ http })} + /> + ); + }; +} + +function isAlertDisabled(alert: Alert) { + return alert.enabled === false; +} + +function isAlertMuted(alert: Alert) { + return alert.muteAll === true; +} diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/types.ts b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/types.ts index ed63ade903104c..7fb7d0bf48e4d2 100644 --- a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/types.ts +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/types.ts @@ -4,8 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ import { TypeRegistry } from './application/type_registry'; -import { SanitizedAlert as Alert } from '../../../alerting/common'; -export { SanitizedAlert as Alert, AlertAction } from '../../../alerting/common'; +import { SanitizedAlert as Alert, AlertAction } from '../../../alerting/common'; +import { ActionType } from '../../../../../plugins/actions/common'; + +export { Alert, AlertAction }; +export { ActionType }; export type ActionTypeIndex = Record; export type AlertTypeIndex = Record; @@ -47,11 +50,6 @@ export interface ValidationResult { errors: Record; } -export interface ActionType { - id: string; - name: string; -} - export interface ActionConnector { secrets: Record; id: string; diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts new file mode 100644 index 00000000000000..e8ed54571c77cd --- /dev/null +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts @@ -0,0 +1,146 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import uuid from 'uuid'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default ({ getPageObjects, getService }: FtrProviderContext) => { + const testSubjects = getService('testSubjects'); + const pageObjects = getPageObjects(['common', 'triggersActionsUI', 'header', 'alertDetailsUI']); + const browser = getService('browser'); + const alerting = getService('alerting'); + + describe('Alert Details', function() { + const testRunUuid = uuid.v4(); + + before(async () => { + await pageObjects.common.navigateToApp('triggersActions'); + + const actions = await Promise.all([ + alerting.actions.createAction({ + name: `server-log-${testRunUuid}-${0}`, + actionTypeId: '.server-log', + config: {}, + secrets: {}, + }), + alerting.actions.createAction({ + name: `server-log-${testRunUuid}-${1}`, + actionTypeId: '.server-log', + config: {}, + secrets: {}, + }), + ]); + + const alert = await alerting.alerts.createAlwaysFiringWithActions( + `test-alert-${testRunUuid}`, + actions.map(action => ({ + id: action.id, + group: 'default', + params: { + message: 'from alert 1s', + level: 'warn', + }, + })) + ); + + // refresh to see alert + await browser.refresh(); + + await pageObjects.header.waitUntilLoadingHasFinished(); + + // Verify content + await testSubjects.existOrFail('alertsList'); + + // click on first alert + await pageObjects.triggersActionsUI.clickOnAlertInAlertsList(alert.name); + }); + + it('renders the alert details', async () => { + const headingText = await pageObjects.alertDetailsUI.getHeadingText(); + expect(headingText).to.be(`test-alert-${testRunUuid}`); + + const alertType = await pageObjects.alertDetailsUI.getAlertType(); + expect(alertType).to.be(`Always Firing`); + + const { actionType, actionCount } = await pageObjects.alertDetailsUI.getActionsLabels(); + expect(actionType).to.be(`Server log`); + expect(actionCount).to.be(`+1`); + }); + + it('should disable the alert', async () => { + const enableSwitch = await testSubjects.find('enableSwitch'); + + const isChecked = await enableSwitch.getAttribute('aria-checked'); + expect(isChecked).to.eql('true'); + + await enableSwitch.click(); + + const enabledSwitchAfterDisabling = await testSubjects.find('enableSwitch'); + const isCheckedAfterDisabling = await enabledSwitchAfterDisabling.getAttribute( + 'aria-checked' + ); + expect(isCheckedAfterDisabling).to.eql('false'); + }); + + it('shouldnt allow you to mute a disabled alert', async () => { + const disabledEnableSwitch = await testSubjects.find('enableSwitch'); + expect(await disabledEnableSwitch.getAttribute('aria-checked')).to.eql('false'); + + const muteSwitch = await testSubjects.find('muteSwitch'); + expect(await muteSwitch.getAttribute('aria-checked')).to.eql('false'); + + await muteSwitch.click(); + + const muteSwitchAfterTryingToMute = await testSubjects.find('muteSwitch'); + const isDisabledMuteAfterDisabling = await muteSwitchAfterTryingToMute.getAttribute( + 'aria-checked' + ); + expect(isDisabledMuteAfterDisabling).to.eql('false'); + }); + + it('should reenable a disabled the alert', async () => { + const enableSwitch = await testSubjects.find('enableSwitch'); + + const isChecked = await enableSwitch.getAttribute('aria-checked'); + expect(isChecked).to.eql('false'); + + await enableSwitch.click(); + + const enabledSwitchAfterReenabling = await testSubjects.find('enableSwitch'); + const isCheckedAfterDisabling = await enabledSwitchAfterReenabling.getAttribute( + 'aria-checked' + ); + expect(isCheckedAfterDisabling).to.eql('true'); + }); + + it('should mute the alert', async () => { + const muteSwitch = await testSubjects.find('muteSwitch'); + + const isChecked = await muteSwitch.getAttribute('aria-checked'); + expect(isChecked).to.eql('false'); + + await muteSwitch.click(); + + const muteSwitchAfterDisabling = await testSubjects.find('muteSwitch'); + const isCheckedAfterDisabling = await muteSwitchAfterDisabling.getAttribute('aria-checked'); + expect(isCheckedAfterDisabling).to.eql('true'); + }); + + it('should unmute the alert', async () => { + const muteSwitch = await testSubjects.find('muteSwitch'); + + const isChecked = await muteSwitch.getAttribute('aria-checked'); + expect(isChecked).to.eql('true'); + + await muteSwitch.click(); + + const muteSwitchAfterUnmuting = await testSubjects.find('muteSwitch'); + const isCheckedAfterDisabling = await muteSwitchAfterUnmuting.getAttribute('aria-checked'); + expect(isCheckedAfterDisabling).to.eql('false'); + }); + }); +}; diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/home_page.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/home_page.ts index 13f50a505b0b6f..307f39382a2363 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/home_page.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/home_page.ts @@ -12,6 +12,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const pageObjects = getPageObjects(['common', 'triggersActionsUI', 'header']); const log = getService('log'); const browser = getService('browser'); + const alerting = getService('alerting'); describe('Home page', function() { before(async () => { @@ -55,6 +56,43 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { // Verify content await testSubjects.existOrFail('alertsList'); }); + + it('navigates to an alert details page', async () => { + const action = await alerting.actions.createAction({ + name: `server-log-${Date.now()}`, + actionTypeId: '.server-log', + config: {}, + secrets: {}, + }); + + const alert = await alerting.alerts.createAlwaysFiringWithAction( + `test-alert-${Date.now()}`, + { + id: action.id, + group: 'default', + params: { + message: 'from alert 1s', + level: 'warn', + }, + } + ); + + // refresh to see alert + await browser.refresh(); + + await pageObjects.header.waitUntilLoadingHasFinished(); + + // Verify content + await testSubjects.existOrFail('alertsList'); + + // click on first alert + await pageObjects.triggersActionsUI.clickOnAlertInAlertsList(alert.name); + + // Verify url + expect(await browser.getCurrentUrl()).to.contain(`/alert/${alert.id}`); + + await alerting.alerts.deleteAlert(alert.id); + }); }); }); }; diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/index.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/index.ts index c76f477c8cfbef..a771fbf85e0b6e 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/index.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/index.ts @@ -12,5 +12,6 @@ export default ({ loadTestFile, getService }: FtrProviderContext) => { loadTestFile(require.resolve('./home_page')); loadTestFile(require.resolve('./connectors')); loadTestFile(require.resolve('./alerts')); + loadTestFile(require.resolve('./details')); }); }; diff --git a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/index.ts b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/index.ts index df651c67c2c281..43162e92563703 100644 --- a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/index.ts +++ b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/index.ts @@ -12,13 +12,42 @@ export default function(kibana: any) { require: ['alerting'], name: 'alerts', init(server: any) { - const noopAlertType: AlertType = { - id: 'test.noop', - name: 'Test: Noop', - actionGroups: ['default'], - async executor() {}, - }; - server.plugins.alerting.setup.registerType(noopAlertType); + createNoopAlertType(server.plugins.alerting.setup); + createAlwaysFiringAlertType(server.plugins.alerting.setup); }, }); } + +function createNoopAlertType(setupContract: any) { + const noopAlertType: AlertType = { + id: 'test.noop', + name: 'Test: Noop', + actionGroups: ['default'], + async executor() {}, + }; + setupContract.registerType(noopAlertType); +} + +function createAlwaysFiringAlertType(setupContract: any) { + // Alert types + const alwaysFiringAlertType: any = { + id: 'test.always-firing', + name: 'Always Firing', + actionGroups: ['default', 'other'], + async executor(alertExecutorOptions: any) { + const { services, state } = alertExecutorOptions; + + services + .alertInstanceFactory('1') + .replaceState({ instanceStateValue: true }) + .scheduleActions('default', { + instanceContextValue: true, + }); + return { + globalStateValue: true, + groupInSeriesIndex: (state.groupInSeriesIndex || 0) + 1, + }; + }, + }; + setupContract.registerType(alwaysFiringAlertType); +} diff --git a/x-pack/test/functional_with_es_ssl/page_objects/alert_details.ts b/x-pack/test/functional_with_es_ssl/page_objects/alert_details.ts new file mode 100644 index 00000000000000..6d2038a6ba04c3 --- /dev/null +++ b/x-pack/test/functional_with_es_ssl/page_objects/alert_details.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../ftr_provider_context'; + +export function AlertDetailsPageProvider({ getService }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + + return { + async getHeadingText() { + return await testSubjects.getVisibleText('alertDetailsTitle'); + }, + async getAlertType() { + return await testSubjects.getVisibleText('alertTypeLabel'); + }, + async getActionsLabels() { + return { + actionType: await testSubjects.getVisibleText('actionTypeLabel'), + actionCount: await testSubjects.getVisibleText('actionCountLabel'), + }; + }, + }; +} diff --git a/x-pack/test/functional_with_es_ssl/page_objects/index.ts b/x-pack/test/functional_with_es_ssl/page_objects/index.ts index a068ba7dfe81d3..cfc44221a9c17f 100644 --- a/x-pack/test/functional_with_es_ssl/page_objects/index.ts +++ b/x-pack/test/functional_with_es_ssl/page_objects/index.ts @@ -6,8 +6,10 @@ import { pageObjects as xpackFunctionalPageObjects } from '../../functional/page_objects'; import { TriggersActionsPageProvider } from './triggers_actions_ui_page'; +import { AlertDetailsPageProvider } from './alert_details'; export const pageObjects = { ...xpackFunctionalPageObjects, triggersActionsUI: TriggersActionsPageProvider, + alertDetailsUI: AlertDetailsPageProvider, }; diff --git a/x-pack/test/functional_with_es_ssl/page_objects/triggers_actions_ui_page.ts b/x-pack/test/functional_with_es_ssl/page_objects/triggers_actions_ui_page.ts index a04ecc969a7e1f..ae66ac0ddddfb7 100644 --- a/x-pack/test/functional_with_es_ssl/page_objects/triggers_actions_ui_page.ts +++ b/x-pack/test/functional_with_es_ssl/page_objects/triggers_actions_ui_page.ts @@ -92,6 +92,10 @@ export function TriggersActionsPageProvider({ getService }: FtrProviderContext) }; }); }, + async clickOnAlertInAlertsList(name: string) { + await this.searchAlerts(name); + await find.clickDisplayedByCssSelector(`[data-test-subj="alertsList"] [title="${name}"]`); + }, async changeTabs(tab: 'alertsTab' | 'connectorsTab') { return await testSubjects.click(tab); }, diff --git a/x-pack/test/functional_with_es_ssl/services/alerting/actions.ts b/x-pack/test/functional_with_es_ssl/services/alerting/actions.ts new file mode 100644 index 00000000000000..9454a32757068b --- /dev/null +++ b/x-pack/test/functional_with_es_ssl/services/alerting/actions.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import axios, { AxiosInstance } from 'axios'; +import util from 'util'; +import { ToolingLog } from '@kbn/dev-utils'; + +export class Actions { + private log: ToolingLog; + private axios: AxiosInstance; + + constructor(url: string, log: ToolingLog) { + this.log = log; + this.axios = axios.create({ + headers: { 'kbn-xsrf': 'x-pack/ftr/services/alerting/actions' }, + baseURL: url, + maxRedirects: 0, + validateStatus: () => true, // we do our own validation below and throw better error messages + }); + } + + public async createAction(actionParams: { + name: string; + actionTypeId: string; + config: Record; + secrets: Record; + }) { + this.log.debug(`creating action ${actionParams.name}`); + + const { data: action, status: actionStatus, actionStatusText } = await this.axios.post( + `/api/action`, + actionParams + ); + if (actionStatus !== 200) { + throw new Error( + `Expected status code of 200, received ${actionStatus} ${actionStatusText}: ${util.inspect( + action + )}` + ); + } + + this.log.debug(`created action ${action.id}`); + return action; + } +} diff --git a/x-pack/test/functional_with_es_ssl/services/alerting/alerts.ts b/x-pack/test/functional_with_es_ssl/services/alerting/alerts.ts new file mode 100644 index 00000000000000..1a31d4796d5bc4 --- /dev/null +++ b/x-pack/test/functional_with_es_ssl/services/alerting/alerts.ts @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import axios, { AxiosInstance } from 'axios'; +import util from 'util'; +import { ToolingLog } from '@kbn/dev-utils'; + +export class Alerts { + private log: ToolingLog; + private axios: AxiosInstance; + + constructor(url: string, log: ToolingLog) { + this.log = log; + this.axios = axios.create({ + headers: { 'kbn-xsrf': 'x-pack/ftr/services/alerting/alerts' }, + baseURL: url, + maxRedirects: 0, + validateStatus: () => true, // we do our own validation below and throw better error messages + }); + } + + public async createAlwaysFiringWithActions( + name: string, + actions: Array<{ + id: string; + group: string; + params: Record; + }> + ) { + this.log.debug(`creating alert ${name}`); + + const { data: alert, status, statusText } = await this.axios.post(`/api/alert`, { + enabled: true, + name, + tags: ['foo'], + alertTypeId: 'test.always-firing', + consumer: 'bar', + schedule: { interval: '1m' }, + throttle: '1m', + actions, + params: {}, + }); + if (status !== 200) { + throw new Error( + `Expected status code of 200, received ${status} ${statusText}: ${util.inspect(alert)}` + ); + } + + this.log.debug(`created alert ${alert.id}`); + + return alert; + } + + public async createAlwaysFiringWithAction( + name: string, + action: { + id: string; + group: string; + params: Record; + } + ) { + return this.createAlwaysFiringWithActions(name, [action]); + } + + public async deleteAlert(id: string) { + this.log.debug(`deleting alert ${id}`); + + const { data: alert, status, statusText } = await this.axios.delete(`/api/alert/${id}`); + if (status !== 204) { + throw new Error( + `Expected status code of 204, received ${status} ${statusText}: ${util.inspect(alert)}` + ); + } + this.log.debug(`deleted alert ${alert.id}`); + } +} diff --git a/x-pack/test/functional_with_es_ssl/services/alerting/index.ts b/x-pack/test/functional_with_es_ssl/services/alerting/index.ts new file mode 100644 index 00000000000000..e0aa827316c01a --- /dev/null +++ b/x-pack/test/functional_with_es_ssl/services/alerting/index.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { format as formatUrl } from 'url'; + +import { Alerts } from './alerts'; +import { Actions } from './actions'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export function AlertsServiceProvider({ getService }: FtrProviderContext) { + const log = getService('log'); + const config = getService('config'); + const url = formatUrl(config.get('servers.kibana')); + + return new (class AlertingService { + actions = new Actions(url, log); + alerts = new Alerts(url, log); + })(); +} diff --git a/x-pack/test/functional_with_es_ssl/services/index.ts b/x-pack/test/functional_with_es_ssl/services/index.ts index 6e96921c25a316..f04c2c980055d3 100644 --- a/x-pack/test/functional_with_es_ssl/services/index.ts +++ b/x-pack/test/functional_with_es_ssl/services/index.ts @@ -5,7 +5,9 @@ */ import { services as xpackFunctionalServices } from '../../functional/services'; +import { AlertsServiceProvider } from './alerting'; export const services = { ...xpackFunctionalServices, + alerting: AlertsServiceProvider, }; From 38dc1cbd3cb74baeb498978688c92683d42b974e Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Mon, 3 Feb 2020 12:03:50 +0100 Subject: [PATCH 22/33] Migrate es-archiver to typescript (#56008) * migrate lib/archives and lib/docs * migrate lib/indices * migrate end of /lib * migrate /actions * migrate es_archiver * migrate cli * migrate tests * use proper log stub * add Record typing Co-authored-by: Elastic Machine --- .../lib/config/read_config_file.ts | 2 +- src/es_archiver/actions/{edit.js => edit.ts} | 13 +- ..._kibana_index.js => empty_kibana_index.ts} | 18 +- .../actions/{index.js => index.ts} | 0 src/es_archiver/actions/{load.js => load.ts} | 29 +++- .../{rebuild_all.js => rebuild_all.ts} | 21 ++- src/es_archiver/actions/{save.js => save.ts} | 24 ++- .../actions/{unload.js => unload.ts} | 22 ++- src/es_archiver/{cli.js => cli.ts} | 22 +-- src/es_archiver/es_archiver.d.ts | 39 ----- .../{es_archiver.js => es_archiver.ts} | 33 ++-- src/es_archiver/index.js | 20 --- src/es_archiver/{index.d.ts => index.ts} | 0 .../lib/__tests__/{stats.js => stats.ts} | 18 +- .../__tests__/{format.js => format.ts} | 16 +- .../archives/__tests__/{parse.js => parse.ts} | 20 +-- .../archives/{constants.js => constants.ts} | 0 .../archives/{filenames.js => filenames.ts} | 6 +- .../lib/archives/{format.js => format.ts} | 4 +- .../lib/archives/{index.js => index.ts} | 2 - .../lib/archives/{parse.js => parse.ts} | 4 +- .../lib/{directory.js => directory.ts} | 5 +- ...ream.js => generate_doc_records_stream.ts} | 2 +- ..._stream.js => index_doc_records_stream.ts} | 4 +- .../lib/docs/__tests__/{stubs.js => stubs.ts} | 29 +++- ...ream.js => generate_doc_records_stream.ts} | 19 ++- .../lib/docs/{index.js => index.ts} | 0 ..._stream.js => index_doc_records_stream.ts} | 9 +- src/es_archiver/lib/{index.js => index.ts} | 2 +- ...index_stream.js => create_index_stream.ts} | 51 +++--- ...index_stream.js => delete_index_stream.ts} | 29 ++-- ...am.js => generate_index_records_stream.ts} | 14 +- .../lib/indices/__tests__/stubs.js | 128 --------------- .../lib/indices/__tests__/stubs.ts | 154 ++++++++++++++++++ ...index_stream.js => create_index_stream.ts} | 35 +++- .../{delete_index.js => delete_index.ts} | 34 ++-- ...index_stream.js => delete_index_stream.ts} | 10 +- ...am.js => generate_index_records_stream.ts} | 8 +- .../lib/indices/{index.js => index.ts} | 0 .../{kibana_index.js => kibana_index.ts} | 96 +++++++---- ...rds_stream.js => filter_records_stream.ts} | 2 +- ...rds_stream.js => filter_records_stream.ts} | 4 +- .../lib/records/{index.js => index.ts} | 0 src/es_archiver/lib/stats.ts | 2 + src/legacy/utils/index.d.ts | 13 ++ src/legacy/utils/streams/index.d.ts | 6 +- 46 files changed, 576 insertions(+), 393 deletions(-) rename src/es_archiver/actions/{edit.js => edit.ts} (91%) rename src/es_archiver/actions/{empty_kibana_index.js => empty_kibana_index.ts} (73%) rename src/es_archiver/actions/{index.js => index.ts} (100%) rename src/es_archiver/actions/{load.js => load.ts} (84%) rename src/es_archiver/actions/{rebuild_all.js => rebuild_all.ts} (84%) rename src/es_archiver/actions/{save.js => save.ts} (83%) rename src/es_archiver/actions/{unload.js => unload.ts} (79%) rename src/es_archiver/{cli.js => cli.ts} (90%) delete mode 100644 src/es_archiver/es_archiver.d.ts rename src/es_archiver/{es_archiver.js => es_archiver.ts} (83%) delete mode 100644 src/es_archiver/index.js rename src/es_archiver/{index.d.ts => index.ts} (100%) rename src/es_archiver/lib/__tests__/{stats.js => stats.ts} (95%) rename src/es_archiver/lib/archives/__tests__/{format.js => format.ts} (89%) rename src/es_archiver/lib/archives/__tests__/{parse.js => parse.ts} (93%) rename src/es_archiver/lib/archives/{constants.js => constants.ts} (100%) rename src/es_archiver/lib/archives/{filenames.js => filenames.ts} (91%) rename src/es_archiver/lib/archives/{format.js => format.ts} (93%) rename src/es_archiver/lib/archives/{index.js => index.ts} (99%) rename src/es_archiver/lib/archives/{parse.js => parse.ts} (91%) rename src/es_archiver/lib/{directory.js => directory.ts} (88%) rename src/es_archiver/lib/docs/__tests__/{generate_doc_records_stream.js => generate_doc_records_stream.ts} (98%) rename src/es_archiver/lib/docs/__tests__/{index_doc_records_stream.js => index_doc_records_stream.ts} (98%) rename src/es_archiver/lib/docs/__tests__/{stubs.js => stubs.ts} (74%) rename src/es_archiver/lib/docs/{generate_doc_records_stream.js => generate_doc_records_stream.ts} (80%) rename src/es_archiver/lib/docs/{index.js => index.ts} (100%) rename src/es_archiver/lib/docs/{index_doc_records_stream.js => index_doc_records_stream.ts} (86%) rename src/es_archiver/lib/{index.js => index.ts} (96%) rename src/es_archiver/lib/indices/__tests__/{create_index_stream.js => create_index_stream.ts} (76%) rename src/es_archiver/lib/indices/__tests__/{delete_index_stream.js => delete_index_stream.ts} (66%) rename src/es_archiver/lib/indices/__tests__/{generate_index_records_stream.js => generate_index_records_stream.ts} (89%) delete mode 100644 src/es_archiver/lib/indices/__tests__/stubs.js create mode 100644 src/es_archiver/lib/indices/__tests__/stubs.ts rename src/es_archiver/lib/indices/{create_index_stream.js => create_index_stream.ts} (81%) rename src/es_archiver/lib/indices/{delete_index.js => delete_index.ts} (76%) rename src/es_archiver/lib/indices/{delete_index_stream.js => delete_index_stream.ts} (86%) rename src/es_archiver/lib/indices/{generate_index_records_stream.js => generate_index_records_stream.ts} (89%) rename src/es_archiver/lib/indices/{index.js => index.ts} (100%) rename src/es_archiver/lib/indices/{kibana_index.js => kibana_index.ts} (70%) rename src/es_archiver/lib/records/__tests__/{filter_records_stream.js => filter_records_stream.ts} (97%) rename src/es_archiver/lib/records/{filter_records_stream.js => filter_records_stream.ts} (91%) rename src/es_archiver/lib/records/{index.js => index.ts} (100%) diff --git a/packages/kbn-test/src/functional_test_runner/lib/config/read_config_file.ts b/packages/kbn-test/src/functional_test_runner/lib/config/read_config_file.ts index 72926cae7dbc4b..96fd525efa3ec8 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/config/read_config_file.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/config/read_config_file.ts @@ -56,7 +56,7 @@ async function getSettingsFromFile(log: ToolingLog, path: string, settingOverrid return transformDeprecations(settingsWithDefaults, logDeprecation); } -export async function readConfigFile(log: ToolingLog, path: string, settingOverrides: any) { +export async function readConfigFile(log: ToolingLog, path: string, settingOverrides: any = {}) { return new Config({ settings: await getSettingsFromFile(log, path, settingOverrides), primary: true, diff --git a/src/es_archiver/actions/edit.js b/src/es_archiver/actions/edit.ts similarity index 91% rename from src/es_archiver/actions/edit.js rename to src/es_archiver/actions/edit.ts index 5e3a3490133c71..de63081a1ea1b5 100644 --- a/src/es_archiver/actions/edit.js +++ b/src/es_archiver/actions/edit.ts @@ -22,12 +22,23 @@ import Fs from 'fs'; import { createGunzip, createGzip, Z_BEST_COMPRESSION } from 'zlib'; import { promisify } from 'util'; import globby from 'globby'; +import { ToolingLog } from '@kbn/dev-utils'; import { createPromiseFromStreams } from '../../legacy/utils'; const unlinkAsync = promisify(Fs.unlink); -export async function editAction({ prefix, dataDir, log, handler }) { +export async function editAction({ + prefix, + dataDir, + log, + handler, +}: { + prefix: string; + dataDir: string; + log: ToolingLog; + handler: () => Promise; +}) { const archives = ( await globby('**/*.gz', { cwd: prefix ? resolve(dataDir, prefix) : dataDir, diff --git a/src/es_archiver/actions/empty_kibana_index.js b/src/es_archiver/actions/empty_kibana_index.ts similarity index 73% rename from src/es_archiver/actions/empty_kibana_index.js rename to src/es_archiver/actions/empty_kibana_index.ts index 386863ec18a43d..5f96fbc5f996ca 100644 --- a/src/es_archiver/actions/empty_kibana_index.js +++ b/src/es_archiver/actions/empty_kibana_index.ts @@ -16,13 +16,25 @@ * specific language governing permissions and limitations * under the License. */ + +import { Client } from 'elasticsearch'; +import { ToolingLog, KbnClient } from '@kbn/dev-utils'; + import { migrateKibanaIndex, deleteKibanaIndices, createStats } from '../lib'; -export async function emptyKibanaIndexAction({ client, log, kbnClient }) { +export async function emptyKibanaIndexAction({ + client, + log, + kbnClient, +}: { + client: Client; + log: ToolingLog; + kbnClient: KbnClient; +}) { const stats = createStats('emptyKibanaIndex', log); const kibanaPluginIds = await kbnClient.plugins.getEnabledIds(); - await deleteKibanaIndices({ client, stats }); - await migrateKibanaIndex({ client, log, stats, kibanaPluginIds }); + await deleteKibanaIndices({ client, stats, log }); + await migrateKibanaIndex({ client, log, kibanaPluginIds }); return stats; } diff --git a/src/es_archiver/actions/index.js b/src/es_archiver/actions/index.ts similarity index 100% rename from src/es_archiver/actions/index.js rename to src/es_archiver/actions/index.ts diff --git a/src/es_archiver/actions/load.js b/src/es_archiver/actions/load.ts similarity index 84% rename from src/es_archiver/actions/load.js rename to src/es_archiver/actions/load.ts index ea02ce9dd3ad33..404fd0daea91d4 100644 --- a/src/es_archiver/actions/load.js +++ b/src/es_archiver/actions/load.ts @@ -19,6 +19,9 @@ import { resolve } from 'path'; import { createReadStream } from 'fs'; +import { Readable } from 'stream'; +import { ToolingLog, KbnClient } from '@kbn/dev-utils'; +import { Client } from 'elasticsearch'; import { createPromiseFromStreams, concatStreamProviders } from '../../legacy/utils'; @@ -38,12 +41,26 @@ import { // pipe a series of streams into each other so that data and errors // flow from the first stream to the last. Errors from the last stream // are not listened for -const pipeline = (...streams) => +const pipeline = (...streams: Readable[]) => streams.reduce((source, dest) => - source.once('error', error => dest.emit('error', error)).pipe(dest) + source.once('error', error => dest.emit('error', error)).pipe(dest as any) ); -export async function loadAction({ name, skipExisting, client, dataDir, log, kbnClient }) { +export async function loadAction({ + name, + skipExisting, + client, + dataDir, + log, + kbnClient, +}: { + name: string; + skipExisting: boolean; + client: Client; + dataDir: string; + log: ToolingLog; + kbnClient: KbnClient; +}) { const inputDir = resolve(dataDir, name); const stats = createStats(name, log); const files = prioritizeMappings(await readDirectory(inputDir)); @@ -64,12 +81,12 @@ export async function loadAction({ name, skipExisting, client, dataDir, log, kbn { objectMode: true } ); - const progress = new Progress('load progress'); + const progress = new Progress(); progress.activate(log); await createPromiseFromStreams([ recordStream, - createCreateIndexStream({ client, stats, skipExisting, log, kibanaPluginIds }), + createCreateIndexStream({ client, stats, skipExisting, log }), createIndexDocRecordsStream(client, stats, progress), ]); @@ -77,7 +94,7 @@ export async function loadAction({ name, skipExisting, client, dataDir, log, kbn const result = stats.toJSON(); for (const [index, { docs }] of Object.entries(result)) { - if (!docs && docs.indexed > 0) { + if (docs && docs.indexed > 0) { log.info('[%s] Indexed %d docs into %j', name, docs.indexed, index); } } diff --git a/src/es_archiver/actions/rebuild_all.js b/src/es_archiver/actions/rebuild_all.ts similarity index 84% rename from src/es_archiver/actions/rebuild_all.js rename to src/es_archiver/actions/rebuild_all.ts index 9379a29c38130c..1467a1d0430b76 100644 --- a/src/es_archiver/actions/rebuild_all.js +++ b/src/es_archiver/actions/rebuild_all.ts @@ -18,13 +18,12 @@ */ import { resolve, dirname, relative } from 'path'; - import { stat, rename, createReadStream, createWriteStream } from 'fs'; - +import { Readable, Writable } from 'stream'; import { fromNode } from 'bluebird'; +import { ToolingLog } from '@kbn/dev-utils'; import { createPromiseFromStreams } from '../../legacy/utils'; - import { prioritizeMappings, readDirectory, @@ -33,12 +32,20 @@ import { createFormatArchiveStreams, } from '../lib'; -async function isDirectory(path) { +async function isDirectory(path: string): Promise { const stats = await fromNode(cb => stat(path, cb)); return stats.isDirectory(); } -export async function rebuildAllAction({ dataDir, log, rootDir = dataDir }) { +export async function rebuildAllAction({ + dataDir, + log, + rootDir = dataDir, +}: { + dataDir: string; + log: ToolingLog; + rootDir?: string; +}) { const childNames = prioritizeMappings(await readDirectory(dataDir)); for (const childName of childNames) { const childPath = resolve(dataDir, childName); @@ -58,11 +65,11 @@ export async function rebuildAllAction({ dataDir, log, rootDir = dataDir }) { const tempFile = childPath + (gzip ? '.rebuilding.gz' : '.rebuilding'); await createPromiseFromStreams([ - createReadStream(childPath), + createReadStream(childPath) as Readable, ...createParseArchiveStreams({ gzip }), ...createFormatArchiveStreams({ gzip }), createWriteStream(tempFile), - ]); + ] as [Readable, ...Writable[]]); await fromNode(cb => rename(tempFile, childPath, cb)); log.info(`${archiveName} Rebuilt ${childName}`); diff --git a/src/es_archiver/actions/save.js b/src/es_archiver/actions/save.ts similarity index 83% rename from src/es_archiver/actions/save.js rename to src/es_archiver/actions/save.ts index 2c264ed2ee3a9e..7a3a9dd97c0ab1 100644 --- a/src/es_archiver/actions/save.js +++ b/src/es_archiver/actions/save.ts @@ -19,9 +19,11 @@ import { resolve } from 'path'; import { createWriteStream, mkdirSync } from 'fs'; +import { Readable, Writable } from 'stream'; +import { Client } from 'elasticsearch'; +import { ToolingLog } from '@kbn/dev-utils'; import { createListStream, createPromiseFromStreams } from '../../legacy/utils'; - import { createStats, createGenerateIndexRecordsStream, @@ -30,7 +32,21 @@ import { Progress, } from '../lib'; -export async function saveAction({ name, indices, client, dataDir, log, raw }) { +export async function saveAction({ + name, + indices, + client, + dataDir, + log, + raw, +}: { + name: string; + indices: string | string[]; + client: Client; + dataDir: string; + log: ToolingLog; + raw: boolean; +}) { const outputDir = resolve(dataDir, name); const stats = createStats(name, log); @@ -48,7 +64,7 @@ export async function saveAction({ name, indices, client, dataDir, log, raw }) { createGenerateIndexRecordsStream(client, stats), ...createFormatArchiveStreams(), createWriteStream(resolve(outputDir, 'mappings.json')), - ]), + ] as [Readable, ...Writable[]]), // export all documents from matching indexes into data.json.gz createPromiseFromStreams([ @@ -56,7 +72,7 @@ export async function saveAction({ name, indices, client, dataDir, log, raw }) { createGenerateDocRecordsStream(client, stats, progress), ...createFormatArchiveStreams({ gzip: !raw }), createWriteStream(resolve(outputDir, `data.json${raw ? '' : '.gz'}`)), - ]), + ] as [Readable, ...Writable[]]), ]); progress.deactivate(); diff --git a/src/es_archiver/actions/unload.js b/src/es_archiver/actions/unload.ts similarity index 79% rename from src/es_archiver/actions/unload.js rename to src/es_archiver/actions/unload.ts index 2acf8d2d719865..130a6b542b218a 100644 --- a/src/es_archiver/actions/unload.js +++ b/src/es_archiver/actions/unload.ts @@ -19,9 +19,11 @@ import { resolve } from 'path'; import { createReadStream } from 'fs'; +import { Readable, Writable } from 'stream'; +import { Client } from 'elasticsearch'; +import { ToolingLog, KbnClient } from '@kbn/dev-utils'; import { createPromiseFromStreams } from '../../legacy/utils'; - import { isGzip, createStats, @@ -32,7 +34,19 @@ import { createDeleteIndexStream, } from '../lib'; -export async function unloadAction({ name, client, dataDir, log, kbnClient }) { +export async function unloadAction({ + name, + client, + dataDir, + log, + kbnClient, +}: { + name: string; + client: Client; + dataDir: string; + log: ToolingLog; + kbnClient: KbnClient; +}) { const inputDir = resolve(dataDir, name); const stats = createStats(name, log); const kibanaPluginIds = await kbnClient.plugins.getEnabledIds(); @@ -42,11 +56,11 @@ export async function unloadAction({ name, client, dataDir, log, kbnClient }) { log.info('[%s] Unloading indices from %j', name, filename); await createPromiseFromStreams([ - createReadStream(resolve(inputDir, filename)), + createReadStream(resolve(inputDir, filename)) as Readable, ...createParseArchiveStreams({ gzip: isGzip(filename) }), createFilterRecordsStream('index'), createDeleteIndexStream(client, stats, log, kibanaPluginIds), - ]); + ] as [Readable, ...Writable[]]); } return stats.toJSON(); diff --git a/src/es_archiver/cli.js b/src/es_archiver/cli.ts similarity index 90% rename from src/es_archiver/cli.js rename to src/es_archiver/cli.ts index 56d1fdca89780c..252f99f8f47afb 100644 --- a/src/es_archiver/cli.js +++ b/src/es_archiver/cli.ts @@ -17,7 +17,7 @@ * under the License. */ -/************************************************************* +/** *********************************************************** * * Run `node scripts/es_archiver --help` for usage information * @@ -27,17 +27,17 @@ import { resolve } from 'path'; import { readFileSync } from 'fs'; import { format as formatUrl } from 'url'; import readline from 'readline'; - import { Command } from 'commander'; import * as legacyElasticsearch from 'elasticsearch'; -import { EsArchiver } from './es_archiver'; import { ToolingLog } from '@kbn/dev-utils'; import { readConfigFile } from '@kbn/test'; +import { EsArchiver } from './es_archiver'; + const cmd = new Command('node scripts/es_archiver'); -const resolveConfigPath = v => resolve(process.cwd(), v); +const resolveConfigPath = (v: string) => resolve(process.cwd(), v); const defaultConfigPath = resolveConfigPath('test/functional/config.js'); cmd @@ -56,6 +56,7 @@ cmd defaultConfigPath ) .on('--help', () => { + // eslint-disable-next-line no-console console.log(readFileSync(resolve(__dirname, './cli_help.txt'), 'utf8')); }); @@ -95,10 +96,10 @@ cmd output: process.stdout, }); - await new Promise(resolve => { + await new Promise(resolveInput => { rl.question(`Press enter when you're done`, () => { rl.close(); - resolve(); + resolveInput(); }); }); }) @@ -112,12 +113,12 @@ cmd cmd.parse(process.argv); -const missingCommand = cmd.args.every(a => !(a instanceof Command)); +const missingCommand = cmd.args.every(a => !((a as any) instanceof Command)); if (missingCommand) { execute(); } -async function execute(fn) { +async function execute(fn?: (esArchiver: EsArchiver, command: Command) => void): Promise { try { const log = new ToolingLog({ level: cmd.verbose ? 'debug' : 'info', @@ -134,7 +135,7 @@ async function execute(fn) { // log and count all validation errors let errorCount = 0; - const error = msg => { + const error = (msg: string) => { errorCount++; log.error(msg); }; @@ -170,11 +171,12 @@ async function execute(fn) { dataDir: resolve(cmd.dir), kibanaUrl: cmd.kibanaUrl, }); - await fn(esArchiver, cmd); + await fn!(esArchiver, cmd); } finally { await client.close(); } } catch (err) { + // eslint-disable-next-line no-console console.log('FATAL ERROR', err.stack); } } diff --git a/src/es_archiver/es_archiver.d.ts b/src/es_archiver/es_archiver.d.ts deleted file mode 100644 index c50ae19d99cbf3..00000000000000 --- a/src/es_archiver/es_archiver.d.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { ToolingLog } from '@kbn/dev-utils'; -import { Client } from 'elasticsearch'; -import { createStats } from './lib/stats'; - -export type JsonStats = ReturnType['toJSON']>; - -export class EsArchiver { - constructor(options: { client: Client; dataDir: string; log: ToolingLog; kibanaUrl: string }); - public save( - name: string, - indices: string | string[], - options?: { raw?: boolean } - ): Promise; - public load(name: string, options?: { skipExisting?: boolean }): Promise; - public unload(name: string): Promise; - public rebuildAll(): Promise; - public edit(prefix: string, handler: () => Promise): Promise; - public loadIfNeeded(name: string): Promise; - public emptyKibanaIndex(): Promise; -} diff --git a/src/es_archiver/es_archiver.js b/src/es_archiver/es_archiver.ts similarity index 83% rename from src/es_archiver/es_archiver.js rename to src/es_archiver/es_archiver.ts index 705706d0e58778..5614dfd842087b 100644 --- a/src/es_archiver/es_archiver.js +++ b/src/es_archiver/es_archiver.ts @@ -17,7 +17,8 @@ * under the License. */ -import { KbnClient } from '@kbn/dev-utils'; +import { Client } from 'elasticsearch'; +import { ToolingLog, KbnClient } from '@kbn/dev-utils'; import { saveAction, @@ -29,7 +30,22 @@ import { } from './actions'; export class EsArchiver { - constructor({ client, dataDir, log, kibanaUrl }) { + private readonly client: Client; + private readonly dataDir: string; + private readonly log: ToolingLog; + private readonly kbnClient: KbnClient; + + constructor({ + client, + dataDir, + log, + kibanaUrl, + }: { + client: Client; + dataDir: string; + log: ToolingLog; + kibanaUrl: string; + }) { this.client = client; this.dataDir = dataDir; this.log = log; @@ -46,7 +62,7 @@ export class EsArchiver { * @property {Boolean} options.raw - should the archive be raw (unzipped) or not * @return Promise */ - async save(name, indices, { raw = false } = {}) { + async save(name: string, indices: string | string[], { raw = false }: { raw?: boolean } = {}) { return await saveAction({ name, indices, @@ -66,9 +82,7 @@ export class EsArchiver { * be ignored or overwritten * @return Promise */ - async load(name, options = {}) { - const { skipExisting } = options; - + async load(name: string, { skipExisting = false }: { skipExisting?: boolean } = {}) { return await loadAction({ name, skipExisting: !!skipExisting, @@ -85,7 +99,7 @@ export class EsArchiver { * @param {String} name * @return Promise */ - async unload(name) { + async unload(name: string) { return await unloadAction({ name, client: this.client, @@ -103,7 +117,6 @@ export class EsArchiver { */ async rebuildAll() { return await rebuildAllAction({ - client: this.client, dataDir: this.dataDir, log: this.log, }); @@ -117,7 +130,7 @@ export class EsArchiver { * @param {() => Promise} handler * @return Promise */ - async edit(prefix, handler) { + async edit(prefix: string, handler: () => Promise) { return await editAction({ prefix, log: this.log, @@ -132,7 +145,7 @@ export class EsArchiver { * @param {String} name * @return Promise */ - async loadIfNeeded(name) { + async loadIfNeeded(name: string) { return await this.load(name, { skipExisting: true }); } diff --git a/src/es_archiver/index.js b/src/es_archiver/index.js deleted file mode 100644 index f7a579a98a42d9..00000000000000 --- a/src/es_archiver/index.js +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export { EsArchiver } from './es_archiver'; diff --git a/src/es_archiver/index.d.ts b/src/es_archiver/index.ts similarity index 100% rename from src/es_archiver/index.d.ts rename to src/es_archiver/index.ts diff --git a/src/es_archiver/lib/__tests__/stats.js b/src/es_archiver/lib/__tests__/stats.ts similarity index 95% rename from src/es_archiver/lib/__tests__/stats.js rename to src/es_archiver/lib/__tests__/stats.ts index ccc24c25fb8601..28e337b3da5298 100644 --- a/src/es_archiver/lib/__tests__/stats.js +++ b/src/es_archiver/lib/__tests__/stats.ts @@ -17,26 +17,26 @@ * under the License. */ -import expect from '@kbn/expect'; import { uniq } from 'lodash'; import sinon from 'sinon'; +import expect from '@kbn/expect'; +import { ToolingLog } from '@kbn/dev-utils'; import { createStats } from '../'; -import { ToolingLog } from '@kbn/dev-utils'; -function createBufferedLog() { - const log = new ToolingLog({ +function createBufferedLog(): ToolingLog & { buffer: string } { + const log: ToolingLog = new ToolingLog({ level: 'debug', writeTo: { - write: chunk => (log.buffer += chunk), + write: chunk => ((log as any).buffer += chunk), }, }); - log.buffer = ''; - return log; + (log as any).buffer = ''; + return log as ToolingLog & { buffer: string }; } -function assertDeepClones(a, b) { - const path = []; +function assertDeepClones(a: any, b: any) { + const path: string[] = []; try { (function recurse(one, two) { if (typeof one !== 'object' || typeof two !== 'object') { diff --git a/src/es_archiver/lib/archives/__tests__/format.js b/src/es_archiver/lib/archives/__tests__/format.ts similarity index 89% rename from src/es_archiver/lib/archives/__tests__/format.js rename to src/es_archiver/lib/archives/__tests__/format.ts index 20ead30824d064..f472f094134d7b 100644 --- a/src/es_archiver/lib/archives/__tests__/format.js +++ b/src/es_archiver/lib/archives/__tests__/format.ts @@ -17,7 +17,7 @@ * under the License. */ -import Stream from 'stream'; +import Stream, { Readable, Writable } from 'stream'; import { createGunzip } from 'zlib'; import expect from '@kbn/expect'; @@ -43,11 +43,11 @@ describe('esArchiver createFormatArchiveStreams', () => { }); it('streams consume js values and produces buffers', async () => { - const output = await createPromiseFromStreams([ + const output = await createPromiseFromStreams([ createListStream(INPUTS), ...createFormatArchiveStreams({ gzip: false }), createConcatStream([]), - ]); + ] as [Readable, ...Writable[]]); expect(output.length).to.be.greaterThan(0); output.forEach(b => expect(b).to.be.a(Buffer)); @@ -58,7 +58,7 @@ describe('esArchiver createFormatArchiveStreams', () => { createListStream(INPUTS), ...createFormatArchiveStreams({ gzip: false }), createConcatStream(''), - ]); + ] as [Readable, ...Writable[]]); expect(json).to.be(INPUT_JSON); }); @@ -73,11 +73,11 @@ describe('esArchiver createFormatArchiveStreams', () => { }); it('streams consume js values and produces buffers', async () => { - const output = await createPromiseFromStreams([ + const output = await createPromiseFromStreams([ createListStream([1, 2, { foo: 'bar' }, [1, 2]]), ...createFormatArchiveStreams({ gzip: true }), createConcatStream([]), - ]); + ] as [Readable, ...Writable[]]); expect(output.length).to.be.greaterThan(0); output.forEach(b => expect(b).to.be.a(Buffer)); @@ -89,7 +89,7 @@ describe('esArchiver createFormatArchiveStreams', () => { ...createFormatArchiveStreams({ gzip: true }), createGunzip(), createConcatStream(''), - ]); + ] as [Readable, ...Writable[]]); expect(output).to.be(INPUT_JSON); }); }); @@ -100,7 +100,7 @@ describe('esArchiver createFormatArchiveStreams', () => { createListStream(INPUTS), ...createFormatArchiveStreams(), createConcatStream(''), - ]); + ] as [Readable, ...Writable[]]); expect(json).to.be(INPUT_JSON); }); diff --git a/src/es_archiver/lib/archives/__tests__/parse.js b/src/es_archiver/lib/archives/__tests__/parse.ts similarity index 93% rename from src/es_archiver/lib/archives/__tests__/parse.js rename to src/es_archiver/lib/archives/__tests__/parse.ts index 2e1506e543a358..ba30156b5af393 100644 --- a/src/es_archiver/lib/archives/__tests__/parse.js +++ b/src/es_archiver/lib/archives/__tests__/parse.ts @@ -17,7 +17,7 @@ * under the License. */ -import Stream, { PassThrough, Transform } from 'stream'; +import Stream, { PassThrough, Readable, Writable, Transform } from 'stream'; import { createGzip } from 'zlib'; import expect from '@kbn/expect'; @@ -66,13 +66,13 @@ describe('esArchiver createParseArchiveStreams', () => { ]), ...createParseArchiveStreams({ gzip: false }), createConcatStream([]), - ]); + ] as [Readable, ...Writable[]]); expect(output).to.eql([{ a: 1 }, 1]); }); it('provides each JSON object as soon as it is parsed', async () => { - let onReceived; + let onReceived: (resolved: any) => void; const receivedPromise = new Promise(resolve => (onReceived = resolve)); const input = new PassThrough(); const check = new Transform({ @@ -80,16 +80,16 @@ describe('esArchiver createParseArchiveStreams', () => { readableObjectMode: true, transform(chunk, env, callback) { onReceived(chunk); - callback(null, chunk); + callback(undefined, chunk); }, }); const finalPromise = createPromiseFromStreams([ - input, + input as Readable, ...createParseArchiveStreams(), check, createConcatStream([]), - ]); + ] as [Readable, ...Writable[]]); input.write(Buffer.from('{"a": 1}\n\n{"a":')); expect(await receivedPromise).to.eql({ a: 1 }); @@ -110,7 +110,7 @@ describe('esArchiver createParseArchiveStreams', () => { ]), ...createParseArchiveStreams({ gzip: false }), createConcatStream(), - ]); + ] as [Readable, ...Writable[]]); throw new Error('should have failed'); } catch (err) { expect(err.message).to.contain('Unexpected number'); @@ -149,7 +149,7 @@ describe('esArchiver createParseArchiveStreams', () => { createGzip(), ...createParseArchiveStreams({ gzip: true }), createConcatStream([]), - ]); + ] as [Readable, ...Writable[]]); expect(output).to.eql([{ a: 1 }, { a: 2 }]); }); @@ -161,7 +161,7 @@ describe('esArchiver createParseArchiveStreams', () => { createGzip(), ...createParseArchiveStreams({ gzip: true }), createConcatStream([]), - ]); + ] as [Readable, ...Writable[]]); expect(output).to.eql([]); }); @@ -173,7 +173,7 @@ describe('esArchiver createParseArchiveStreams', () => { createListStream([Buffer.from('{"a": 1}')]), ...createParseArchiveStreams({ gzip: true }), createConcatStream(), - ]); + ] as [Readable, ...Writable[]]); throw new Error('should have failed'); } catch (err) { expect(err.message).to.contain('incorrect header check'); diff --git a/src/es_archiver/lib/archives/constants.js b/src/es_archiver/lib/archives/constants.ts similarity index 100% rename from src/es_archiver/lib/archives/constants.js rename to src/es_archiver/lib/archives/constants.ts diff --git a/src/es_archiver/lib/archives/filenames.js b/src/es_archiver/lib/archives/filenames.ts similarity index 91% rename from src/es_archiver/lib/archives/filenames.js rename to src/es_archiver/lib/archives/filenames.ts index 4ced04401d28d5..24c355edda278a 100644 --- a/src/es_archiver/lib/archives/filenames.js +++ b/src/es_archiver/lib/archives/filenames.ts @@ -19,7 +19,7 @@ import { basename, extname } from 'path'; -export function isGzip(path) { +export function isGzip(path: string) { return extname(path) === '.gz'; } @@ -28,7 +28,7 @@ export function isGzip(path) { * @param {String} path * @return {Boolean} */ -export function isMappingFile(path) { +export function isMappingFile(path: string) { return basename(path, '.gz') === 'mappings.json'; } @@ -41,7 +41,7 @@ export function isMappingFile(path) { * @param {Array} filenames * @return {Array} */ -export function prioritizeMappings(filenames) { +export function prioritizeMappings(filenames: string[]) { return filenames.slice().sort((fa, fb) => { if (isMappingFile(fa) === isMappingFile(fb)) return 0; return isMappingFile(fb) ? 1 : -1; diff --git a/src/es_archiver/lib/archives/format.js b/src/es_archiver/lib/archives/format.ts similarity index 93% rename from src/es_archiver/lib/archives/format.js rename to src/es_archiver/lib/archives/format.ts index 01fca87e7ba988..9bef4c9adbf059 100644 --- a/src/es_archiver/lib/archives/format.js +++ b/src/es_archiver/lib/archives/format.ts @@ -19,14 +19,12 @@ import { createGzip, Z_BEST_COMPRESSION } from 'zlib'; import { PassThrough } from 'stream'; - import stringify from 'json-stable-stringify'; import { createMapStream, createIntersperseStream } from '../../../legacy/utils'; - import { RECORD_SEPARATOR } from './constants'; -export function createFormatArchiveStreams({ gzip = false } = {}) { +export function createFormatArchiveStreams({ gzip = false }: { gzip?: boolean } = {}) { return [ createMapStream(record => stringify(record, { space: ' ' })), createIntersperseStream(RECORD_SEPARATOR), diff --git a/src/es_archiver/lib/archives/index.js b/src/es_archiver/lib/archives/index.ts similarity index 99% rename from src/es_archiver/lib/archives/index.js rename to src/es_archiver/lib/archives/index.ts index 4020f52e45a35b..6aa489ea5a46de 100644 --- a/src/es_archiver/lib/archives/index.js +++ b/src/es_archiver/lib/archives/index.ts @@ -18,7 +18,5 @@ */ export { isGzip, prioritizeMappings } from './filenames'; - export { createParseArchiveStreams } from './parse'; - export { createFormatArchiveStreams } from './format'; diff --git a/src/es_archiver/lib/archives/parse.js b/src/es_archiver/lib/archives/parse.ts similarity index 91% rename from src/es_archiver/lib/archives/parse.js rename to src/es_archiver/lib/archives/parse.ts index 4fe1df72592292..0f4460c925019d 100644 --- a/src/es_archiver/lib/archives/parse.js +++ b/src/es_archiver/lib/archives/parse.ts @@ -29,7 +29,7 @@ export function createParseArchiveStreams({ gzip = false } = {}) { gzip ? createGunzip() : new PassThrough(), createReplaceStream('\r\n', '\n'), createSplitStream(RECORD_SEPARATOR), - createFilterStream(l => l.match(/[^\s]/)), - createMapStream(json => JSON.parse(json.trim())), + createFilterStream(l => !!l.match(/[^\s]/)), + createMapStream(json => JSON.parse(json.trim())), ]; } diff --git a/src/es_archiver/lib/directory.js b/src/es_archiver/lib/directory.ts similarity index 88% rename from src/es_archiver/lib/directory.js rename to src/es_archiver/lib/directory.ts index 5aee10cfea65d9..8581207fa795d1 100644 --- a/src/es_archiver/lib/directory.js +++ b/src/es_archiver/lib/directory.ts @@ -18,10 +18,9 @@ */ import { readdir } from 'fs'; - import { fromNode } from 'bluebird'; -export async function readDirectory(path) { - const allNames = await fromNode(cb => readdir(path, cb)); +export async function readDirectory(path: string) { + const allNames = await fromNode(cb => readdir(path, cb)); return allNames.filter(name => !name.startsWith('.')); } diff --git a/src/es_archiver/lib/docs/__tests__/generate_doc_records_stream.js b/src/es_archiver/lib/docs/__tests__/generate_doc_records_stream.ts similarity index 98% rename from src/es_archiver/lib/docs/__tests__/generate_doc_records_stream.js rename to src/es_archiver/lib/docs/__tests__/generate_doc_records_stream.ts index bf4aab208127f4..03599cdc9fbcfe 100644 --- a/src/es_archiver/lib/docs/__tests__/generate_doc_records_stream.js +++ b/src/es_archiver/lib/docs/__tests__/generate_doc_records_stream.ts @@ -143,7 +143,7 @@ describe('esArchiver: createGenerateDocRecordsStream()', () => { }, }, ]); - sinon.assert.calledTwice(stats.archivedDoc); + sinon.assert.calledTwice(stats.archivedDoc as any); expect(progress.getTotal()).to.be(2); expect(progress.getComplete()).to.be(2); }); diff --git a/src/es_archiver/lib/docs/__tests__/index_doc_records_stream.js b/src/es_archiver/lib/docs/__tests__/index_doc_records_stream.ts similarity index 98% rename from src/es_archiver/lib/docs/__tests__/index_doc_records_stream.js rename to src/es_archiver/lib/docs/__tests__/index_doc_records_stream.ts index 2535642c27cc95..35b068a6910907 100644 --- a/src/es_archiver/lib/docs/__tests__/index_doc_records_stream.js +++ b/src/es_archiver/lib/docs/__tests__/index_doc_records_stream.ts @@ -26,12 +26,12 @@ import { Progress } from '../../progress'; import { createIndexDocRecordsStream } from '../index_doc_records_stream'; import { createStubStats, createStubClient, createPersonDocRecords } from './stubs'; -const recordsToBulkBody = records => { +const recordsToBulkBody = (records: any[]) => { return records.reduce((acc, record) => { const { index, id, source } = record.value; return [...acc, { index: { _index: index, _id: id } }, source]; - }, []); + }, [] as any[]); }; describe('esArchiver: createIndexDocRecordsStream()', () => { diff --git a/src/es_archiver/lib/docs/__tests__/stubs.js b/src/es_archiver/lib/docs/__tests__/stubs.ts similarity index 74% rename from src/es_archiver/lib/docs/__tests__/stubs.js rename to src/es_archiver/lib/docs/__tests__/stubs.ts index 9ed48efa7d03ac..698d62e450cb46 100644 --- a/src/es_archiver/lib/docs/__tests__/stubs.js +++ b/src/es_archiver/lib/docs/__tests__/stubs.ts @@ -17,17 +17,22 @@ * under the License. */ +import { Client } from 'elasticsearch'; import sinon from 'sinon'; import Chance from 'chance'; import { times } from 'lodash'; + +import { Stats } from '../../stats'; + const chance = new Chance(); -export const createStubStats = () => ({ - indexedDoc: sinon.stub(), - archivedDoc: sinon.stub(), -}); +export const createStubStats = (): Stats => + ({ + indexedDoc: sinon.stub(), + archivedDoc: sinon.stub(), + } as any); -export const createPersonDocRecords = n => +export const createPersonDocRecords = (n: number) => times(n, () => ({ type: 'doc', value: { @@ -42,15 +47,21 @@ export const createPersonDocRecords = n => }, })); -export const createStubClient = (responses = []) => { - const createStubClientMethod = name => +type MockClient = Client & { + assertNoPendingResponses: () => void; +}; + +export const createStubClient = ( + responses: Array<(name: string, params: any) => any | Promise> = [] +): MockClient => { + const createStubClientMethod = (name: string) => sinon.spy(async params => { if (responses.length === 0) { throw new Error(`unexpected client.${name} call`); } const response = responses.shift(); - return await response(name, params); + return await response!(name, params); }); return { @@ -63,5 +74,5 @@ export const createStubClient = (responses = []) => { throw new Error(`There are ${responses.length} unsent responses.`); } }, - }; + } as any; }; diff --git a/src/es_archiver/lib/docs/generate_doc_records_stream.js b/src/es_archiver/lib/docs/generate_doc_records_stream.ts similarity index 80% rename from src/es_archiver/lib/docs/generate_doc_records_stream.js rename to src/es_archiver/lib/docs/generate_doc_records_stream.ts index be8b0351d95c8e..e255a0abc36c5f 100644 --- a/src/es_archiver/lib/docs/generate_doc_records_stream.js +++ b/src/es_archiver/lib/docs/generate_doc_records_stream.ts @@ -18,33 +18,36 @@ */ import { Transform } from 'stream'; +import { Client, SearchParams, SearchResponse } from 'elasticsearch'; +import { Stats } from '../stats'; +import { Progress } from '../progress'; const SCROLL_SIZE = 1000; const SCROLL_TIMEOUT = '1m'; -export function createGenerateDocRecordsStream(client, stats, progress) { +export function createGenerateDocRecordsStream(client: Client, stats: Stats, progress: Progress) { return new Transform({ writableObjectMode: true, readableObjectMode: true, async transform(index, enc, callback) { try { - let remainingHits = null; - let resp = null; + let remainingHits = 0; + let resp: SearchResponse | null = null; while (!resp || remainingHits > 0) { if (!resp) { resp = await client.search({ - index: index, + index, scroll: SCROLL_TIMEOUT, size: SCROLL_SIZE, _source: true, - rest_total_hits_as_int: true, - }); + rest_total_hits_as_int: true, // not declared on SearchParams type + } as SearchParams); remainingHits = resp.hits.total; progress.addToTotal(remainingHits); } else { resp = await client.scroll({ - scrollId: resp._scroll_id, + scrollId: resp._scroll_id!, scroll: SCROLL_TIMEOUT, }); } @@ -68,7 +71,7 @@ export function createGenerateDocRecordsStream(client, stats, progress) { progress.addToComplete(resp.hits.hits.length); } - callback(null); + callback(undefined); } catch (err) { callback(err); } diff --git a/src/es_archiver/lib/docs/index.js b/src/es_archiver/lib/docs/index.ts similarity index 100% rename from src/es_archiver/lib/docs/index.js rename to src/es_archiver/lib/docs/index.ts diff --git a/src/es_archiver/lib/docs/index_doc_records_stream.js b/src/es_archiver/lib/docs/index_doc_records_stream.ts similarity index 86% rename from src/es_archiver/lib/docs/index_doc_records_stream.js rename to src/es_archiver/lib/docs/index_doc_records_stream.ts index 73fb75c52ff0a6..8236ae8adb6db7 100644 --- a/src/es_archiver/lib/docs/index_doc_records_stream.js +++ b/src/es_archiver/lib/docs/index_doc_records_stream.ts @@ -17,11 +17,14 @@ * under the License. */ +import { Client } from 'elasticsearch'; import { Writable } from 'stream'; +import { Stats } from '../stats'; +import { Progress } from '../progress'; -export function createIndexDocRecordsStream(client, stats, progress) { - async function indexDocs(docs) { - const body = []; +export function createIndexDocRecordsStream(client: Client, stats: Stats, progress: Progress) { + async function indexDocs(docs: any[]) { + const body: any[] = []; docs.forEach(doc => { stats.indexedDoc(doc.index); diff --git a/src/es_archiver/lib/index.js b/src/es_archiver/lib/index.ts similarity index 96% rename from src/es_archiver/lib/index.js rename to src/es_archiver/lib/index.ts index 246dd8169cd6b0..960d51e411859e 100644 --- a/src/es_archiver/lib/index.js +++ b/src/es_archiver/lib/index.ts @@ -30,7 +30,7 @@ export { export { createFilterRecordsStream } from './records'; -export { createStats } from './stats'; +export { createStats, Stats } from './stats'; export { isGzip, diff --git a/src/es_archiver/lib/indices/__tests__/create_index_stream.js b/src/es_archiver/lib/indices/__tests__/create_index_stream.ts similarity index 76% rename from src/es_archiver/lib/indices/__tests__/create_index_stream.js rename to src/es_archiver/lib/indices/__tests__/create_index_stream.ts index 9e0f83c9f7eb96..c90497eded88ce 100644 --- a/src/es_archiver/lib/indices/__tests__/create_index_stream.js +++ b/src/es_archiver/lib/indices/__tests__/create_index_stream.ts @@ -34,10 +34,13 @@ import { createStubIndexRecord, createStubDocRecord, createStubClient, + createStubLogger, } from './stubs'; const chance = new Chance(); +const log = createStubLogger(); + describe('esArchiver: createCreateIndexStream()', () => { describe('defaults', () => { it('deletes existing indices, creates all', async () => { @@ -48,15 +51,15 @@ describe('esArchiver: createCreateIndexStream()', () => { createStubIndexRecord('existing-index'), createStubIndexRecord('new-index'), ]), - createCreateIndexStream({ client, stats }), + createCreateIndexStream({ client, stats, log }), ]); expect(stats.getTestSummary()).to.eql({ deletedIndex: 1, createdIndex: 2, }); - sinon.assert.callCount(client.indices.delete, 1); - sinon.assert.callCount(client.indices.create, 3); // one failed create because of existing + sinon.assert.callCount(client.indices.delete as sinon.SinonSpy, 1); + sinon.assert.callCount(client.indices.create as sinon.SinonSpy, 3); // one failed create because of existing }); it('deletes existing aliases, creates all', async () => { @@ -67,14 +70,19 @@ describe('esArchiver: createCreateIndexStream()', () => { createStubIndexRecord('existing-index'), createStubIndexRecord('new-index'), ]), - createCreateIndexStream({ client, stats, log: { debug: () => {} } }), + createCreateIndexStream({ client, stats, log }), ]); - expect(client.indices.getAlias.calledOnce).to.be.ok(); - expect(client.indices.getAlias.args[0][0]).to.eql({ name: 'existing-index', ignore: [404] }); - expect(client.indices.delete.calledOnce).to.be.ok(); - expect(client.indices.delete.args[0][0]).to.eql({ index: ['actual-index'] }); - sinon.assert.callCount(client.indices.create, 3); // one failed create because of existing + expect((client.indices.getAlias as sinon.SinonSpy).calledOnce).to.be.ok(); + expect((client.indices.getAlias as sinon.SinonSpy).args[0][0]).to.eql({ + name: 'existing-index', + ignore: [404], + }); + expect((client.indices.delete as sinon.SinonSpy).calledOnce).to.be.ok(); + expect((client.indices.delete as sinon.SinonSpy).args[0][0]).to.eql({ + index: ['actual-index'], + }); + sinon.assert.callCount(client.indices.create as sinon.SinonSpy, 3); // one failed create because of existing }); it('passes through "hit" records', async () => { @@ -86,7 +94,7 @@ describe('esArchiver: createCreateIndexStream()', () => { createStubDocRecord('index', 1), createStubDocRecord('index', 2), ]), - createCreateIndexStream({ client, stats }), + createCreateIndexStream({ client, stats, log }), createConcatStream([]), ]); @@ -101,11 +109,11 @@ describe('esArchiver: createCreateIndexStream()', () => { createStubIndexRecord('index', { foo: {} }), createStubDocRecord('index', 1), ]), - createCreateIndexStream({ client, stats }), + createCreateIndexStream({ client, stats, log }), createConcatStream([]), ]); - sinon.assert.calledWith(client.indices.create, { + sinon.assert.calledWith(client.indices.create as sinon.SinonSpy, { method: 'PUT', index: 'index', body: { @@ -126,7 +134,7 @@ describe('esArchiver: createCreateIndexStream()', () => { const output = await createPromiseFromStreams([ createListStream([createStubIndexRecord('index'), ...randoms]), - createCreateIndexStream({ client, stats }), + createCreateIndexStream({ client, stats, log }), createConcatStream([]), ]); @@ -140,7 +148,7 @@ describe('esArchiver: createCreateIndexStream()', () => { const output = await createPromiseFromStreams([ createListStream(nonRecordValues), - createCreateIndexStream({ client, stats }), + createCreateIndexStream({ client, stats, log }), createConcatStream([]), ]); @@ -161,6 +169,7 @@ describe('esArchiver: createCreateIndexStream()', () => { createCreateIndexStream({ client, stats, + log, skipExisting: true, }), ]); @@ -169,9 +178,12 @@ describe('esArchiver: createCreateIndexStream()', () => { skippedIndex: 1, createdIndex: 1, }); - sinon.assert.callCount(client.indices.delete, 0); - sinon.assert.callCount(client.indices.create, 2); // one failed create because of existing - expect(client.indices.create.args[0][0]).to.have.property('index', 'new-index'); + sinon.assert.callCount(client.indices.delete as sinon.SinonSpy, 0); + sinon.assert.callCount(client.indices.create as sinon.SinonSpy, 2); // one failed create because of existing + expect((client.indices.create as sinon.SinonSpy).args[0][0]).to.have.property( + 'index', + 'new-index' + ); }); it('filters documents for skipped indices', async () => { @@ -190,6 +202,7 @@ describe('esArchiver: createCreateIndexStream()', () => { createCreateIndexStream({ client, stats, + log, skipExisting: true, }), createConcatStream([]), @@ -199,8 +212,8 @@ describe('esArchiver: createCreateIndexStream()', () => { skippedIndex: 1, createdIndex: 1, }); - sinon.assert.callCount(client.indices.delete, 0); - sinon.assert.callCount(client.indices.create, 2); // one failed create because of existing + sinon.assert.callCount(client.indices.delete as sinon.SinonSpy, 0); + sinon.assert.callCount(client.indices.create as sinon.SinonSpy, 2); // one failed create because of existing expect(output).to.have.length(2); expect(output).to.eql([ diff --git a/src/es_archiver/lib/indices/__tests__/delete_index_stream.js b/src/es_archiver/lib/indices/__tests__/delete_index_stream.ts similarity index 66% rename from src/es_archiver/lib/indices/__tests__/delete_index_stream.js rename to src/es_archiver/lib/indices/__tests__/delete_index_stream.ts index 955d1fff8779ed..1c989ba158a29d 100644 --- a/src/es_archiver/lib/indices/__tests__/delete_index_stream.js +++ b/src/es_archiver/lib/indices/__tests__/delete_index_stream.ts @@ -23,7 +23,14 @@ import { createListStream, createPromiseFromStreams } from '../../../../legacy/u import { createDeleteIndexStream } from '../delete_index_stream'; -import { createStubStats, createStubClient, createStubIndexRecord } from './stubs'; +import { + createStubStats, + createStubClient, + createStubIndexRecord, + createStubLogger, +} from './stubs'; + +const log = createStubLogger(); describe('esArchiver: createDeleteIndexStream()', () => { it('deletes the index without checking if it exists', async () => { @@ -32,13 +39,13 @@ describe('esArchiver: createDeleteIndexStream()', () => { await createPromiseFromStreams([ createListStream([createStubIndexRecord('index1')]), - createDeleteIndexStream(client, stats), + createDeleteIndexStream(client, stats, log, []), ]); - sinon.assert.notCalled(stats.deletedIndex); - sinon.assert.notCalled(client.indices.create); - sinon.assert.calledOnce(client.indices.delete); - sinon.assert.notCalled(client.indices.exists); + sinon.assert.notCalled(stats.deletedIndex as sinon.SinonSpy); + sinon.assert.notCalled(client.indices.create as sinon.SinonSpy); + sinon.assert.calledOnce(client.indices.delete as sinon.SinonSpy); + sinon.assert.notCalled(client.indices.exists as sinon.SinonSpy); }); it('reports the delete when the index existed', async () => { @@ -47,12 +54,12 @@ describe('esArchiver: createDeleteIndexStream()', () => { await createPromiseFromStreams([ createListStream([createStubIndexRecord('index1')]), - createDeleteIndexStream(client, stats), + createDeleteIndexStream(client, stats, log, []), ]); - sinon.assert.calledOnce(stats.deletedIndex); - sinon.assert.notCalled(client.indices.create); - sinon.assert.calledOnce(client.indices.delete); - sinon.assert.notCalled(client.indices.exists); + sinon.assert.calledOnce(stats.deletedIndex as sinon.SinonSpy); + sinon.assert.notCalled(client.indices.create as sinon.SinonSpy); + sinon.assert.calledOnce(client.indices.delete as sinon.SinonSpy); + sinon.assert.notCalled(client.indices.exists as sinon.SinonSpy); }); }); diff --git a/src/es_archiver/lib/indices/__tests__/generate_index_records_stream.js b/src/es_archiver/lib/indices/__tests__/generate_index_records_stream.ts similarity index 89% rename from src/es_archiver/lib/indices/__tests__/generate_index_records_stream.js rename to src/es_archiver/lib/indices/__tests__/generate_index_records_stream.ts index 3523e9e82b153f..7a3712ca1a336f 100644 --- a/src/es_archiver/lib/indices/__tests__/generate_index_records_stream.js +++ b/src/es_archiver/lib/indices/__tests__/generate_index_records_stream.ts @@ -45,10 +45,10 @@ describe('esArchiver: createGenerateIndexRecordsStream()', () => { archivedIndex: 4, }); - sinon.assert.callCount(client.indices.get, 4); - sinon.assert.notCalled(client.indices.create); - sinon.assert.notCalled(client.indices.delete); - sinon.assert.notCalled(client.indices.exists); + sinon.assert.callCount(client.indices.get as sinon.SinonSpy, 4); + sinon.assert.notCalled(client.indices.create as sinon.SinonSpy); + sinon.assert.notCalled(client.indices.delete as sinon.SinonSpy); + sinon.assert.notCalled(client.indices.exists as sinon.SinonSpy); }); it('filters index metadata from settings', async () => { @@ -60,9 +60,9 @@ describe('esArchiver: createGenerateIndexRecordsStream()', () => { createGenerateIndexRecordsStream(client, stats), ]); - const params = client.indices.get.args[0][0]; + const params = (client.indices.get as sinon.SinonSpy).args[0][0]; expect(params).to.have.property('filterPath'); - const filters = params.filterPath; + const filters: string[] = params.filterPath; expect(filters.some(path => path.includes('index.creation_date'))).to.be(true); expect(filters.some(path => path.includes('index.uuid'))).to.be(true); expect(filters.some(path => path.includes('index.version'))).to.be(true); @@ -73,7 +73,7 @@ describe('esArchiver: createGenerateIndexRecordsStream()', () => { const stats = createStubStats(); const client = createStubClient(['index1', 'index2', 'index3']); - const indexRecords = await createPromiseFromStreams([ + const indexRecords = await createPromiseFromStreams([ createListStream(['index1', 'index2', 'index3']), createGenerateIndexRecordsStream(client, stats), createConcatStream([]), diff --git a/src/es_archiver/lib/indices/__tests__/stubs.js b/src/es_archiver/lib/indices/__tests__/stubs.js deleted file mode 100644 index 00649a06f9efef..00000000000000 --- a/src/es_archiver/lib/indices/__tests__/stubs.js +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import sinon from 'sinon'; - -export const createStubStats = () => ({ - createdIndex: sinon.stub(), - createdAliases: sinon.stub(), - deletedIndex: sinon.stub(), - skippedIndex: sinon.stub(), - archivedIndex: sinon.stub(), - getTestSummary() { - const summary = {}; - Object.keys(this).forEach(key => { - if (this[key].callCount) { - summary[key] = this[key].callCount; - } - }); - return summary; - }, -}); - -export const createStubIndexRecord = (index, aliases = {}) => ({ - type: 'index', - value: { index, aliases }, -}); - -export const createStubDocRecord = (index, id) => ({ - type: 'doc', - value: { index, id }, -}); - -const createEsClientError = errorType => { - const err = new Error(`ES Client Error Stub "${errorType}"`); - err.body = { - error: { - type: errorType, - }, - }; - return err; -}; - -const indexAlias = (aliases, index) => Object.keys(aliases).find(k => aliases[k] === index); - -export const createStubClient = (existingIndices = [], aliases = {}) => ({ - indices: { - get: sinon.spy(async ({ index }) => { - if (!existingIndices.includes(index)) { - throw createEsClientError('index_not_found_exception'); - } - - return { - [index]: { - mappings: {}, - settings: {}, - }, - }; - }), - existsAlias: sinon.spy(({ name }) => { - return Promise.resolve(aliases.hasOwnProperty(name)); - }), - getAlias: sinon.spy(async ({ index, name }) => { - if (index && existingIndices.indexOf(index) >= 0) { - const result = indexAlias(aliases, index); - return { [index]: { aliases: result ? { [result]: {} } : {} } }; - } - - if (name && aliases[name]) { - return { [aliases[name]]: { aliases: { [name]: {} } } }; - } - - return { status: 404 }; - }), - updateAliases: sinon.spy(async ({ body }) => { - body.actions.forEach(({ add: { index, alias } }) => { - if (!existingIndices.includes(index)) { - throw createEsClientError('index_not_found_exception'); - } - existingIndices.push({ index, alias }); - }); - - return { ok: true }; - }), - create: sinon.spy(async ({ index }) => { - if (existingIndices.includes(index) || aliases.hasOwnProperty(index)) { - throw createEsClientError('resource_already_exists_exception'); - } else { - existingIndices.push(index); - return { ok: true }; - } - }), - delete: sinon.spy(async ({ index }) => { - const indices = Array.isArray(index) ? index : [index]; - if (indices.every(ix => existingIndices.includes(ix))) { - // Delete aliases associated with our indices - indices.forEach(ix => { - const alias = Object.keys(aliases).find(k => aliases[k] === ix); - if (alias) { - delete aliases[alias]; - } - }); - indices.forEach(ix => existingIndices.splice(existingIndices.indexOf(ix), 1)); - return { ok: true }; - } else { - throw createEsClientError('index_not_found_exception'); - } - }), - exists: sinon.spy(async () => { - throw new Error('Do not use indices.exists(). React to errors instead.'); - }), - }, -}); diff --git a/src/es_archiver/lib/indices/__tests__/stubs.ts b/src/es_archiver/lib/indices/__tests__/stubs.ts new file mode 100644 index 00000000000000..3f4682299c38d8 --- /dev/null +++ b/src/es_archiver/lib/indices/__tests__/stubs.ts @@ -0,0 +1,154 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Client } from 'elasticsearch'; +import sinon from 'sinon'; +import { ToolingLog } from '@kbn/dev-utils'; +import { Stats } from '../../stats'; + +type StubStats = Stats & { + getTestSummary: () => Record; +}; + +export const createStubStats = (): StubStats => + ({ + createdIndex: sinon.stub(), + createdAliases: sinon.stub(), + deletedIndex: sinon.stub(), + skippedIndex: sinon.stub(), + archivedIndex: sinon.stub(), + getTestSummary() { + const summary: Record = {}; + Object.keys(this).forEach(key => { + if (this[key].callCount) { + summary[key] = this[key].callCount; + } + }); + return summary; + }, + } as any); + +export const createStubLogger = (): ToolingLog => + ({ + debug: sinon.stub(), + info: sinon.stub(), + success: sinon.stub(), + warning: sinon.stub(), + error: sinon.stub(), + } as any); + +export const createStubIndexRecord = (index: string, aliases = {}) => ({ + type: 'index', + value: { index, aliases }, +}); + +export const createStubDocRecord = (index: string, id: number) => ({ + type: 'doc', + value: { index, id }, +}); + +const createEsClientError = (errorType: string) => { + const err = new Error(`ES Client Error Stub "${errorType}"`); + (err as any).body = { + error: { + type: errorType, + }, + }; + return err; +}; + +const indexAlias = (aliases: Record, index: string) => + Object.keys(aliases).find(k => aliases[k] === index); + +type StubClient = Client; + +export const createStubClient = ( + existingIndices: string[] = [], + aliases: Record = {} +): StubClient => + ({ + indices: { + get: sinon.spy(async ({ index }) => { + if (!existingIndices.includes(index)) { + throw createEsClientError('index_not_found_exception'); + } + + return { + [index]: { + mappings: {}, + settings: {}, + }, + }; + }), + existsAlias: sinon.spy(({ name }) => { + return Promise.resolve(aliases.hasOwnProperty(name)); + }), + getAlias: sinon.spy(async ({ index, name }) => { + if (index && existingIndices.indexOf(index) >= 0) { + const result = indexAlias(aliases, index); + return { [index]: { aliases: result ? { [result]: {} } : {} } }; + } + + if (name && aliases[name]) { + return { [aliases[name]]: { aliases: { [name]: {} } } }; + } + + return { status: 404 }; + }), + updateAliases: sinon.spy(async ({ body }) => { + body.actions.forEach( + ({ add: { index, alias } }: { add: { index: string; alias: string } }) => { + if (!existingIndices.includes(index)) { + throw createEsClientError('index_not_found_exception'); + } + existingIndices.push({ index, alias } as any); + } + ); + + return { ok: true }; + }), + create: sinon.spy(async ({ index }) => { + if (existingIndices.includes(index) || aliases.hasOwnProperty(index)) { + throw createEsClientError('resource_already_exists_exception'); + } else { + existingIndices.push(index); + return { ok: true }; + } + }), + delete: sinon.spy(async ({ index }) => { + const indices = Array.isArray(index) ? index : [index]; + if (indices.every(ix => existingIndices.includes(ix))) { + // Delete aliases associated with our indices + indices.forEach(ix => { + const alias = Object.keys(aliases).find(k => aliases[k] === ix); + if (alias) { + delete aliases[alias]; + } + }); + indices.forEach(ix => existingIndices.splice(existingIndices.indexOf(ix), 1)); + return { ok: true }; + } else { + throw createEsClientError('index_not_found_exception'); + } + }), + exists: sinon.spy(async () => { + throw new Error('Do not use indices.exists(). React to errors instead.'); + }), + }, + } as any); diff --git a/src/es_archiver/lib/indices/create_index_stream.js b/src/es_archiver/lib/indices/create_index_stream.ts similarity index 81% rename from src/es_archiver/lib/indices/create_index_stream.js rename to src/es_archiver/lib/indices/create_index_stream.ts index 8fe4bc568cd231..df9d3bb623ad65 100644 --- a/src/es_archiver/lib/indices/create_index_stream.js +++ b/src/es_archiver/lib/indices/create_index_stream.ts @@ -17,13 +17,36 @@ * under the License. */ -import { Transform } from 'stream'; - +import { Transform, Readable } from 'stream'; import { get, once } from 'lodash'; +import { Client } from 'elasticsearch'; +import { ToolingLog } from '@kbn/dev-utils'; + +import { Stats } from '../stats'; import { deleteKibanaIndices } from './kibana_index'; import { deleteIndex } from './delete_index'; -export function createCreateIndexStream({ client, stats, skipExisting, log }) { +interface DocRecord { + value: { + index: string; + type: string; + settings: Record; + mappings: Record; + aliases: Record; + }; +} + +export function createCreateIndexStream({ + client, + stats, + skipExisting = false, + log, +}: { + client: Client; + stats: Stats; + skipExisting?: boolean; + log: ToolingLog; +}) { const skipDocsFromIndices = new Set(); // If we're trying to import Kibana index docs, we need to ensure that @@ -31,7 +54,7 @@ export function createCreateIndexStream({ client, stats, skipExisting, log }) { // migrations. This only needs to be done once per archive load operation. const deleteKibanaIndicesOnce = once(deleteKibanaIndices); - async function handleDoc(stream, record) { + async function handleDoc(stream: Readable, record: DocRecord) { if (skipDocsFromIndices.has(record.value.index)) { return; } @@ -39,7 +62,7 @@ export function createCreateIndexStream({ client, stats, skipExisting, log }) { stream.push(record); } - async function handleIndex(record) { + async function handleIndex(record: DocRecord) { const { index, settings, mappings, aliases } = record.value; const isKibana = index.startsWith('.kibana'); @@ -102,7 +125,7 @@ export function createCreateIndexStream({ client, stats, skipExisting, log }) { break; } - callback(null); + callback(); } catch (err) { callback(err); } diff --git a/src/es_archiver/lib/indices/delete_index.js b/src/es_archiver/lib/indices/delete_index.ts similarity index 76% rename from src/es_archiver/lib/indices/delete_index.js rename to src/es_archiver/lib/indices/delete_index.ts index 6f60d9533a36b3..e3fca587fbc3d3 100644 --- a/src/es_archiver/lib/indices/delete_index.js +++ b/src/es_archiver/lib/indices/delete_index.ts @@ -18,22 +18,34 @@ */ import { get } from 'lodash'; +import { Client } from 'elasticsearch'; +import { ToolingLog } from '@kbn/dev-utils'; +import { Stats } from '../stats'; // see https://github.com/elastic/elasticsearch/blob/99f88f15c5febbca2d13b5b5fda27b844153bf1a/server/src/main/java/org/elasticsearch/cluster/SnapshotsInProgress.java#L313-L319 const PENDING_SNAPSHOT_STATUSES = ['INIT', 'STARTED', 'WAITING']; -export async function deleteIndex(options) { +export async function deleteIndex(options: { + client: Client; + stats: Stats; + index: string; + log: ToolingLog; + retryIfSnapshottingCount?: number; +}): Promise { const { client, stats, index, log, retryIfSnapshottingCount = 10 } = options; const getIndicesToDelete = async () => { const aliasInfo = await client.indices.getAlias({ name: index, ignore: [404] }); - return aliasInfo.status === 404 ? index : Object.keys(aliasInfo); + return aliasInfo.status === 404 ? [index] : Object.keys(aliasInfo); }; try { const indicesToDelete = await getIndicesToDelete(); await client.indices.delete({ index: indicesToDelete }); - stats.deletedIndex(indicesToDelete); + for (let i = 0; i < indicesToDelete.length; i++) { + const indexToDelete = indicesToDelete[i]; + stats.deletedIndex(indexToDelete); + } } catch (error) { if (retryIfSnapshottingCount > 0 && isDeleteWhileSnapshotInProgressError(error)) { stats.waitingForInProgressSnapshot(index); @@ -56,7 +68,7 @@ export async function deleteIndex(options) { * @param {Error} error * @return {Boolean} */ -export function isDeleteWhileSnapshotInProgressError(error) { +export function isDeleteWhileSnapshotInProgressError(error: object) { return get(error, 'body.error.reason', '').startsWith( 'Cannot delete indices that are being snapshotted' ); @@ -65,13 +77,9 @@ export function isDeleteWhileSnapshotInProgressError(error) { /** * Wait for the any snapshot in any repository that is * snapshotting this index to complete. - * - * @param {EsClient} client - * @param {string} index the name of the index to look for - * @return {Promise} */ -export async function waitForSnapshotCompletion(client, index, log) { - const isSnapshotPending = async (repository, snapshot) => { +export async function waitForSnapshotCompletion(client: Client, index: string, log: ToolingLog) { + const isSnapshotPending = async (repository: string, snapshot: string) => { const { snapshots: [status], } = await client.snapshot.status({ @@ -83,7 +91,7 @@ export async function waitForSnapshotCompletion(client, index, log) { return PENDING_SNAPSHOT_STATUSES.includes(status.state); }; - const getInProgressSnapshots = async repository => { + const getInProgressSnapshots = async (repository: string) => { const { snapshots: inProgressSnapshots } = await client.snapshot.get({ repository, snapshot: '_current', @@ -91,9 +99,9 @@ export async function waitForSnapshotCompletion(client, index, log) { return inProgressSnapshots; }; - for (const repository of Object.keys(await client.snapshot.getRepository())) { + for (const repository of Object.keys(await client.snapshot.getRepository({} as any))) { const allInProgress = await getInProgressSnapshots(repository); - const found = allInProgress.find(s => s.indices.includes(index)); + const found = allInProgress.find((s: any) => s.indices.includes(index)); if (!found) { continue; diff --git a/src/es_archiver/lib/indices/delete_index_stream.js b/src/es_archiver/lib/indices/delete_index_stream.ts similarity index 86% rename from src/es_archiver/lib/indices/delete_index_stream.js rename to src/es_archiver/lib/indices/delete_index_stream.ts index 31a49ed30a124d..b4e1e530e1f84e 100644 --- a/src/es_archiver/lib/indices/delete_index_stream.js +++ b/src/es_archiver/lib/indices/delete_index_stream.ts @@ -18,11 +18,19 @@ */ import { Transform } from 'stream'; +import { Client } from 'elasticsearch'; +import { ToolingLog } from '@kbn/dev-utils'; +import { Stats } from '../stats'; import { deleteIndex } from './delete_index'; import { cleanKibanaIndices } from './kibana_index'; -export function createDeleteIndexStream(client, stats, log, kibanaPluginIds) { +export function createDeleteIndexStream( + client: Client, + stats: Stats, + log: ToolingLog, + kibanaPluginIds: string[] +) { return new Transform({ readableObjectMode: true, writableObjectMode: true, diff --git a/src/es_archiver/lib/indices/generate_index_records_stream.js b/src/es_archiver/lib/indices/generate_index_records_stream.ts similarity index 89% rename from src/es_archiver/lib/indices/generate_index_records_stream.js rename to src/es_archiver/lib/indices/generate_index_records_stream.ts index 1d1a44aa634c24..b4b98f8ae262c7 100644 --- a/src/es_archiver/lib/indices/generate_index_records_stream.js +++ b/src/es_archiver/lib/indices/generate_index_records_stream.ts @@ -18,14 +18,16 @@ */ import { Transform } from 'stream'; +import { Client } from 'elasticsearch'; +import { Stats } from '../stats'; -export function createGenerateIndexRecordsStream(client, stats) { +export function createGenerateIndexRecordsStream(client: Client, stats: Stats) { return new Transform({ writableObjectMode: true, readableObjectMode: true, async transform(indexOrAlias, enc, callback) { try { - const resp = await client.indices.get({ + const resp = (await client.indices.get({ index: indexOrAlias, filterPath: [ '*.settings', @@ -36,7 +38,7 @@ export function createGenerateIndexRecordsStream(client, stats) { '-*.settings.index.version', '-*.settings.index.provided_name', ], - }); + })) as Record; for (const [index, { settings, mappings }] of Object.entries(resp)) { const { diff --git a/src/es_archiver/lib/indices/index.js b/src/es_archiver/lib/indices/index.ts similarity index 100% rename from src/es_archiver/lib/indices/index.js rename to src/es_archiver/lib/indices/index.ts diff --git a/src/es_archiver/lib/indices/kibana_index.js b/src/es_archiver/lib/indices/kibana_index.ts similarity index 70% rename from src/es_archiver/lib/indices/kibana_index.js rename to src/es_archiver/lib/indices/kibana_index.ts index 744132bdcef69e..de67ba7c4e31ec 100644 --- a/src/es_archiver/lib/indices/kibana_index.js +++ b/src/es_archiver/lib/indices/kibana_index.ts @@ -17,29 +17,34 @@ * under the License. */ -import _ from 'lodash'; +import { get } from 'lodash'; import fs from 'fs'; -import path from 'path'; +import Path from 'path'; import { promisify } from 'util'; import { toArray } from 'rxjs/operators'; +import { Client, CreateDocumentParams } from 'elasticsearch'; +import { ToolingLog } from '@kbn/dev-utils'; +import { Stats } from '../stats'; import { deleteIndex } from './delete_index'; -import { collectUiExports } from '../../../legacy/ui/ui_exports'; import { KibanaMigrator } from '../../../core/server/saved_objects/migrations'; import { SavedObjectsSchema } from '../../../core/server/saved_objects'; +// @ts-ignore +import { collectUiExports } from '../../../legacy/ui/ui_exports'; +// @ts-ignore import { findPluginSpecs } from '../../../legacy/plugin_discovery'; /** * Load the uiExports for a Kibana instance, only load uiExports from xpack if * it is enabled in the Kibana server. */ -const getUiExports = async kibanaPluginIds => { +const getUiExports = async (kibanaPluginIds: string[]) => { const xpackEnabled = kibanaPluginIds.includes('xpack_main'); const { spec$ } = await findPluginSpecs({ plugins: { - scanDirs: [path.resolve(__dirname, '../../../legacy/core_plugins')], - paths: xpackEnabled ? [path.resolve(__dirname, '../../../../x-pack')] : [], + scanDirs: [Path.resolve(__dirname, '../../../legacy/core_plugins')], + paths: xpackEnabled ? [Path.resolve(__dirname, '../../../../x-pack')] : [], }, }); @@ -50,7 +55,15 @@ const getUiExports = async kibanaPluginIds => { /** * Deletes all indices that start with `.kibana` */ -export async function deleteKibanaIndices({ client, stats, log }) { +export async function deleteKibanaIndices({ + client, + stats, + log, +}: { + client: Client; + stats: Stats; + log: ToolingLog; +}) { const indexNames = await fetchKibanaIndices(client); if (!indexNames.length) { return; @@ -76,37 +89,52 @@ export async function deleteKibanaIndices({ client, stats, log }) { * builds up an object that implements just enough of the kbnMigrations interface * as is required by migrations. */ -export async function migrateKibanaIndex({ client, log, kibanaPluginIds }) { +export async function migrateKibanaIndex({ + client, + log, + kibanaPluginIds, +}: { + client: Client; + log: ToolingLog; + kibanaPluginIds: string[]; +}) { const uiExports = await getUiExports(kibanaPluginIds); const kibanaVersion = await loadKibanaVersion(); - const config = { + const config: Record = { 'xpack.task_manager.index': '.kibana_task_manager', }; + const logger = { + trace: log.verbose.bind(log), + debug: log.debug.bind(log), + info: log.info.bind(log), + warn: log.warning.bind(log), + error: log.error.bind(log), + fatal: log.error.bind(log), + log: (entry: any) => log.info(entry.message), + get: () => logger, + }; + const migratorOptions = { - config: { get: path => config[path] }, + config: { get: (path: string) => config[path] } as any, savedObjectsConfig: { scrollDuration: '5m', batchSize: 100, pollInterval: 100, + skip: false, }, kibanaConfig: { index: '.kibana', - }, - logger: { - trace: log.verbose.bind(log), - debug: log.debug.bind(log), - info: log.info.bind(log), - warn: log.warning.bind(log), - error: log.error.bind(log), - }, - version: kibanaVersion, + } as any, + logger, + kibanaVersion, savedObjectSchemas: new SavedObjectsSchema(uiExports.savedObjectSchemas), savedObjectMappings: uiExports.savedObjectMappings, savedObjectMigrations: uiExports.savedObjectMigrations, savedObjectValidations: uiExports.savedObjectValidations, - callCluster: (path, ...args) => _.get(client, path).call(client, ...args), + callCluster: (path: string, ...args: any[]) => + (get(client, path) as Function).call(client, ...args), }; return await new KibanaMigrator(migratorOptions).runMigrations(); @@ -114,8 +142,8 @@ export async function migrateKibanaIndex({ client, log, kibanaPluginIds }) { async function loadKibanaVersion() { const readFile = promisify(fs.readFile); - const packageJson = await readFile(path.join(__dirname, '../../../../package.json')); - return JSON.parse(packageJson).version; + const packageJson = await readFile(Path.join(__dirname, '../../../../package.json')); + return JSON.parse(packageJson.toString('utf-8')).version; } /** @@ -123,16 +151,24 @@ async function loadKibanaVersion() { * .kibana, .kibana_1, .kibana_323, etc. This finds all indices starting * with .kibana, then filters out any that aren't actually Kibana's core * index (e.g. we don't want to remove .kibana_task_manager or the like). - * - * @param {string} index */ -async function fetchKibanaIndices(client) { +async function fetchKibanaIndices(client: Client) { const kibanaIndices = await client.cat.indices({ index: '.kibana*', format: 'json' }); - const isKibanaIndex = index => /^\.kibana(:?_\d*)?$/.test(index); - return kibanaIndices.map(x => x.index).filter(isKibanaIndex); + const isKibanaIndex = (index: string) => /^\.kibana(:?_\d*)?$/.test(index); + return kibanaIndices.map((x: { index: string }) => x.index).filter(isKibanaIndex); } -export async function cleanKibanaIndices({ client, stats, log, kibanaPluginIds }) { +export async function cleanKibanaIndices({ + client, + stats, + log, + kibanaPluginIds, +}: { + client: Client; + stats: Stats; + log: ToolingLog; + kibanaPluginIds: string[]; +}) { if (!kibanaPluginIds.includes('spaces')) { return await deleteKibanaIndices({ client, @@ -178,7 +214,7 @@ export async function cleanKibanaIndices({ client, stats, log, kibanaPluginIds } stats.deletedIndex('.kibana'); } -export async function createDefaultSpace({ index, client }) { +export async function createDefaultSpace({ index, client }: { index: string; client: Client }) { await client.create({ index, id: 'space:default', @@ -193,5 +229,5 @@ export async function createDefaultSpace({ index, client }) { _reserved: true, }, }, - }); + } as CreateDocumentParams); } diff --git a/src/es_archiver/lib/records/__tests__/filter_records_stream.js b/src/es_archiver/lib/records/__tests__/filter_records_stream.ts similarity index 97% rename from src/es_archiver/lib/records/__tests__/filter_records_stream.js rename to src/es_archiver/lib/records/__tests__/filter_records_stream.ts index fd35575ca59bae..d5830478decbae 100644 --- a/src/es_archiver/lib/records/__tests__/filter_records_stream.js +++ b/src/es_archiver/lib/records/__tests__/filter_records_stream.ts @@ -51,7 +51,7 @@ describe('esArchiver: createFilterRecordsStream()', () => { it('produces record values that have a matching type', async () => { const type1 = chance.word({ length: 5 }); - const output = await createPromiseFromStreams([ + const output = await createPromiseFromStreams([ createListStream([ { type: type1, value: {} }, { type: type1, value: {} }, diff --git a/src/es_archiver/lib/records/filter_records_stream.js b/src/es_archiver/lib/records/filter_records_stream.ts similarity index 91% rename from src/es_archiver/lib/records/filter_records_stream.js rename to src/es_archiver/lib/records/filter_records_stream.ts index 5a835ffe8e84d5..191cbd3b921e3b 100644 --- a/src/es_archiver/lib/records/filter_records_stream.js +++ b/src/es_archiver/lib/records/filter_records_stream.ts @@ -19,14 +19,14 @@ import { Transform } from 'stream'; -export function createFilterRecordsStream(type) { +export function createFilterRecordsStream(type: string) { return new Transform({ writableObjectMode: true, readableObjectMode: true, transform(record, enc, callback) { if (record && record.type === type) { - callback(null, record); + callback(undefined, record); } else { callback(); } diff --git a/src/es_archiver/lib/records/index.js b/src/es_archiver/lib/records/index.ts similarity index 100% rename from src/es_archiver/lib/records/index.js rename to src/es_archiver/lib/records/index.ts diff --git a/src/es_archiver/lib/stats.ts b/src/es_archiver/lib/stats.ts index 5f73304abf9a8b..c69b764fc72908 100644 --- a/src/es_archiver/lib/stats.ts +++ b/src/es_archiver/lib/stats.ts @@ -37,6 +37,8 @@ export interface IndexStats { }; } +export type Stats = ReturnType; + export function createStats(name: string, log: ToolingLog) { const info = (msg: string, ...args: any[]) => log.info(`[${name}] ${msg}`, ...args); const debug = (msg: string, ...args: any[]) => log.debug(`[${name}] ${msg}`, ...args); diff --git a/src/legacy/utils/index.d.ts b/src/legacy/utils/index.d.ts index a57caad1d34bf8..c294c79542bbe9 100644 --- a/src/legacy/utils/index.d.ts +++ b/src/legacy/utils/index.d.ts @@ -18,3 +18,16 @@ */ export function unset(object: object, rawPath: string): void; + +export { + concatStreamProviders, + createConcatStream, + createFilterStream, + createIntersperseStream, + createListStream, + createMapStream, + createPromiseFromStreams, + createReduceStream, + createReplaceStream, + createSplitStream, +} from './streams'; diff --git a/src/legacy/utils/streams/index.d.ts b/src/legacy/utils/streams/index.d.ts index b8d4c67050b2da..5ef39b292c6858 100644 --- a/src/legacy/utils/streams/index.d.ts +++ b/src/legacy/utils/streams/index.d.ts @@ -20,17 +20,17 @@ import { Readable, Transform, Writable, TransformOptions } from 'stream'; export function concatStreamProviders( - sourceProviders: Readable[], + sourceProviders: Array<() => Readable>, options: TransformOptions ): Transform; export function createIntersperseStream(intersperseChunk: string | Buffer): Transform; export function createSplitStream(splitChunk: T): Transform; -export function createListStream(items: any[]): Readable; +export function createListStream(items: any | any[]): Readable; export function createReduceStream(reducer: (value: any, chunk: T, enc: string) => T): Transform; export function createPromiseFromStreams([first, ...rest]: [Readable, ...Writable[]]): Promise< T >; -export function createConcatStream(initial: any): Transform; +export function createConcatStream(initial?: any): Transform; export function createMapStream(fn: (value: T, i: number) => void): Transform; export function createReplaceStream(toReplace: string, replacement: string | Buffer): Transform; export function createFilterStream(fn: (obj: T) => boolean): Transform; From 3d96e4c95bdc7ce2401b66fc1c816d826496382a Mon Sep 17 00:00:00 2001 From: patrykkopycinski Date: Mon, 3 Feb 2020 13:24:38 +0100 Subject: [PATCH 23/33] [SIEM] Fix Welcome to SIEM typo (#56582) --- x-pack/legacy/plugins/siem/public/pages/common/translations.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/legacy/plugins/siem/public/pages/common/translations.ts b/x-pack/legacy/plugins/siem/public/pages/common/translations.ts index 3e203383756163..072aee50d5136b 100644 --- a/x-pack/legacy/plugins/siem/public/pages/common/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/common/translations.ts @@ -12,7 +12,7 @@ export const EMPTY_TITLE = i18n.translate('xpack.siem.pages.common.emptyTitle', export const EMPTY_MESSAGE = i18n.translate('xpack.siem.pages.common.emptyMessage', { defaultMessage: - 'To begin using security information and event management, you’ll need to begin adding SIEM-related data to Kibana by installing and configuring our data shippers, called Beats. Let’s do that now!', + 'To begin using security information and event management (SIEM), you’ll need to add SIEM-related data, in Elastic Common Schema (ECS) format, to the Elastic Stack. An easy way to get started is by installing and configuring our data shippers, called Beats. Let’s do that now!', }); export const EMPTY_ACTION_PRIMARY = i18n.translate('xpack.siem.pages.common.emptyActionPrimary', { From 598968f20f1b3b09429dd6eac079b010913f0dee Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Mon, 3 Feb 2020 13:33:19 +0100 Subject: [PATCH 24/33] fix duplicate header warning (#56491) logged when attaching 2+ headers due to excessive check Co-authored-by: Elastic Machine --- src/core/server/http/integration_tests/lifecycle.test.ts | 1 + src/core/server/http/lifecycle/on_pre_response.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/core/server/http/integration_tests/lifecycle.test.ts b/src/core/server/http/integration_tests/lifecycle.test.ts index b16352838fad11..6dc7ece1359df7 100644 --- a/src/core/server/http/integration_tests/lifecycle.test.ts +++ b/src/core/server/http/integration_tests/lifecycle.test.ts @@ -721,6 +721,7 @@ describe('Auth', () => { res.ok({ headers: { 'www-authenticate': 'from handler', + 'another-header': 'yet another header', }, }) ); diff --git a/src/core/server/http/lifecycle/on_pre_response.ts b/src/core/server/http/lifecycle/on_pre_response.ts index 45d7478df98051..50d3d7b47bf8d6 100644 --- a/src/core/server/http/lifecycle/on_pre_response.ts +++ b/src/core/server/http/lifecycle/on_pre_response.ts @@ -120,8 +120,8 @@ export function adoptToHapiOnPreResponseFormat(fn: OnPreResponseHandler, log: Lo ...(result.headers as any), // hapi types don't specify string[] as valid value }; } else { + findHeadersIntersection(response.headers, result.headers, log); for (const [headerName, headerValue] of Object.entries(result.headers)) { - findHeadersIntersection(response.headers, result.headers, log); response.header(headerName, headerValue as any); // hapi types don't specify string[] as valid value } } From 8def60e1dac63ba7d7900bbffca6e1519ff1dfb7 Mon Sep 17 00:00:00 2001 From: Aleh Zasypkin Date: Mon, 3 Feb 2020 14:43:10 +0100 Subject: [PATCH 25/33] Unify Security and EncryptedSavedObjects public contract names according to NP migration guide. (#56597) --- .../plugins/alerting/server/alerts_client.ts | 6 +++--- .../alerting/server/alerts_client_factory.ts | 6 +++--- x-pack/legacy/plugins/alerting/server/shim.ts | 18 +++++++++--------- .../server/task_runner/task_runner_factory.ts | 4 ++-- .../plugins/encrypted_saved_objects/index.ts | 4 ++-- x-pack/legacy/plugins/reporting/index.ts | 2 +- .../legacy/plugins/reporting/server/plugin.ts | 2 +- x-pack/legacy/plugins/siem/server/plugin.ts | 4 ++-- .../actions/server/lib/action_executor.ts | 4 ++-- .../actions/server/lib/task_runner_factory.ts | 4 ++-- x-pack/plugins/actions/server/plugin.ts | 8 ++++---- x-pack/plugins/case/server/plugin.ts | 2 +- x-pack/plugins/case/server/services/index.ts | 5 +---- .../encrypted_saved_objects/server/index.ts | 2 +- .../encrypted_saved_objects/server/mocks.ts | 6 +++--- .../encrypted_saved_objects/server/plugin.ts | 6 +++--- x-pack/plugins/security/server/index.ts | 6 +++--- x-pack/plugins/security/server/mocks.ts | 4 ++-- x-pack/plugins/security/server/plugin.ts | 4 ++-- .../lib/spaces_client/spaces_client.test.ts | 8 ++++---- .../server/lib/spaces_client/spaces_client.ts | 6 +++--- x-pack/plugins/spaces/server/plugin.ts | 2 +- .../server/spaces_service/spaces_service.ts | 2 +- .../common/fixtures/plugins/aad/index.ts | 5 +++-- .../plugins/encrypted_saved_objects/index.ts | 9 +++++---- 25 files changed, 64 insertions(+), 65 deletions(-) diff --git a/x-pack/legacy/plugins/alerting/server/alerts_client.ts b/x-pack/legacy/plugins/alerting/server/alerts_client.ts index a6ba936b76570e..1346d403edda1a 100644 --- a/x-pack/legacy/plugins/alerting/server/alerts_client.ts +++ b/x-pack/legacy/plugins/alerting/server/alerts_client.ts @@ -29,7 +29,7 @@ import { CreateAPIKeyResult as SecurityPluginCreateAPIKeyResult, InvalidateAPIKeyResult as SecurityPluginInvalidateAPIKeyResult, } from '../../../../plugins/security/server'; -import { PluginStartContract as EncryptedSavedObjectsStartContract } from '../../../../plugins/encrypted_saved_objects/server'; +import { EncryptedSavedObjectsPluginStart } from '../../../../plugins/encrypted_saved_objects/server'; import { TaskManagerStartContract } from '../../../../plugins/task_manager/server'; type NormalizedAlertAction = Omit; @@ -45,7 +45,7 @@ interface ConstructorOptions { taskManager: TaskManagerStartContract; savedObjectsClient: SavedObjectsClientContract; alertTypeRegistry: AlertTypeRegistry; - encryptedSavedObjectsPlugin: EncryptedSavedObjectsStartContract; + encryptedSavedObjectsPlugin: EncryptedSavedObjectsPluginStart; spaceId?: string; namespace?: string; getUserName: () => Promise; @@ -120,7 +120,7 @@ export class AlertsClient { private readonly invalidateAPIKey: ( params: InvalidateAPIKeyParams ) => Promise; - encryptedSavedObjectsPlugin: EncryptedSavedObjectsStartContract; + encryptedSavedObjectsPlugin: EncryptedSavedObjectsPluginStart; constructor({ alertTypeRegistry, diff --git a/x-pack/legacy/plugins/alerting/server/alerts_client_factory.ts b/x-pack/legacy/plugins/alerting/server/alerts_client_factory.ts index eab1cc3ce627b4..de789fba0ac388 100644 --- a/x-pack/legacy/plugins/alerting/server/alerts_client_factory.ts +++ b/x-pack/legacy/plugins/alerting/server/alerts_client_factory.ts @@ -11,7 +11,7 @@ import { AlertTypeRegistry, SpaceIdToNamespaceFunction } from './types'; import { SecurityPluginStartContract } from './shim'; import { KibanaRequest, Logger } from '../../../../../src/core/server'; import { InvalidateAPIKeyParams } from '../../../../plugins/security/server'; -import { PluginStartContract as EncryptedSavedObjectsStartContract } from '../../../../plugins/encrypted_saved_objects/server'; +import { EncryptedSavedObjectsPluginStart } from '../../../../plugins/encrypted_saved_objects/server'; import { TaskManagerStartContract } from '../../../../plugins/task_manager/server'; export interface ConstructorOpts { @@ -21,7 +21,7 @@ export interface ConstructorOpts { securityPluginSetup?: SecurityPluginStartContract; getSpaceId: (request: Hapi.Request) => string | undefined; spaceIdToNamespace: SpaceIdToNamespaceFunction; - encryptedSavedObjectsPlugin: EncryptedSavedObjectsStartContract; + encryptedSavedObjectsPlugin: EncryptedSavedObjectsPluginStart; } export class AlertsClientFactory { @@ -31,7 +31,7 @@ export class AlertsClientFactory { private readonly securityPluginSetup?: SecurityPluginStartContract; private readonly getSpaceId: (request: Hapi.Request) => string | undefined; private readonly spaceIdToNamespace: SpaceIdToNamespaceFunction; - private readonly encryptedSavedObjectsPlugin: EncryptedSavedObjectsStartContract; + private readonly encryptedSavedObjectsPlugin: EncryptedSavedObjectsPluginStart; constructor(options: ConstructorOpts) { this.logger = options.logger; diff --git a/x-pack/legacy/plugins/alerting/server/shim.ts b/x-pack/legacy/plugins/alerting/server/shim.ts index 80d01ea722926f..bc8b0eb863634c 100644 --- a/x-pack/legacy/plugins/alerting/server/shim.ts +++ b/x-pack/legacy/plugins/alerting/server/shim.ts @@ -15,10 +15,10 @@ import { getTaskManagerSetup, getTaskManagerStart } from '../../task_manager/ser import { XPackMainPlugin } from '../../xpack_main/server/xpack_main'; import KbnServer from '../../../../../src/legacy/server/kbn_server'; import { - PluginSetupContract as EncryptedSavedObjectsSetupContract, - PluginStartContract as EncryptedSavedObjectsStartContract, + EncryptedSavedObjectsPluginSetup, + EncryptedSavedObjectsPluginStart, } from '../../../../plugins/encrypted_saved_objects/server'; -import { PluginSetupContract as SecurityPlugin } from '../../../../plugins/security/server'; +import { SecurityPluginSetup } from '../../../../plugins/security/server'; import { CoreSetup, LoggerFactory, @@ -44,8 +44,8 @@ export interface Server extends Legacy.Server { /** * Shim what we're thinking setup and start contracts will look like */ -export type SecurityPluginSetupContract = Pick; -export type SecurityPluginStartContract = Pick; +export type SecurityPluginSetupContract = Pick; +export type SecurityPluginStartContract = Pick; export type XPackMainPluginSetupContract = Pick; /** @@ -71,14 +71,14 @@ export interface AlertingPluginsSetup { taskManager: TaskManagerSetupContract; actions: ActionsPluginSetupContract; xpack_main: XPackMainPluginSetupContract; - encryptedSavedObjects: EncryptedSavedObjectsSetupContract; + encryptedSavedObjects: EncryptedSavedObjectsPluginSetup; licensing: LicensingPluginSetup; } export interface AlertingPluginsStart { actions: ActionsPluginStartContract; security?: SecurityPluginStartContract; spaces: () => SpacesPluginStartContract | undefined; - encryptedSavedObjects: EncryptedSavedObjectsStartContract; + encryptedSavedObjects: EncryptedSavedObjectsPluginStart; taskManager: TaskManagerStartContract; } @@ -120,7 +120,7 @@ export function shim( actions: newPlatform.setup.plugins.actions as ActionsPluginSetupContract, xpack_main: server.plugins.xpack_main, encryptedSavedObjects: newPlatform.setup.plugins - .encryptedSavedObjects as EncryptedSavedObjectsSetupContract, + .encryptedSavedObjects as EncryptedSavedObjectsPluginSetup, licensing: newPlatform.setup.plugins.licensing as LicensingPluginSetup, }; @@ -131,7 +131,7 @@ export function shim( // initializes after this function is called spaces: () => server.plugins.spaces, encryptedSavedObjects: newPlatform.start.plugins - .encryptedSavedObjects as EncryptedSavedObjectsStartContract, + .encryptedSavedObjects as EncryptedSavedObjectsPluginStart, taskManager: getTaskManagerStart(server)!, }; diff --git a/x-pack/legacy/plugins/alerting/server/task_runner/task_runner_factory.ts b/x-pack/legacy/plugins/alerting/server/task_runner/task_runner_factory.ts index 67fef33b69c6dd..d2ecfb64c8a813 100644 --- a/x-pack/legacy/plugins/alerting/server/task_runner/task_runner_factory.ts +++ b/x-pack/legacy/plugins/alerting/server/task_runner/task_runner_factory.ts @@ -5,7 +5,7 @@ */ import { Logger } from '../../../../../../src/core/server'; import { RunContext } from '../../../../../plugins/task_manager/server'; -import { PluginStartContract as EncryptedSavedObjectsStartContract } from '../../../../../plugins/encrypted_saved_objects/server'; +import { EncryptedSavedObjectsPluginStart } from '../../../../../plugins/encrypted_saved_objects/server'; import { PluginStartContract as ActionsPluginStartContract } from '../../../../../plugins/actions/server'; import { AlertType, @@ -19,7 +19,7 @@ export interface TaskRunnerContext { logger: Logger; getServices: GetServicesFunction; executeAction: ActionsPluginStartContract['execute']; - encryptedSavedObjectsPlugin: EncryptedSavedObjectsStartContract; + encryptedSavedObjectsPlugin: EncryptedSavedObjectsPluginStart; spaceIdToNamespace: SpaceIdToNamespaceFunction; getBasePath: GetBasePathFunction; } diff --git a/x-pack/legacy/plugins/encrypted_saved_objects/index.ts b/x-pack/legacy/plugins/encrypted_saved_objects/index.ts index 69058a7a33f59f..ce343dba006cfc 100644 --- a/x-pack/legacy/plugins/encrypted_saved_objects/index.ts +++ b/x-pack/legacy/plugins/encrypted_saved_objects/index.ts @@ -6,7 +6,7 @@ import { Root } from 'joi'; import { Legacy } from 'kibana'; -import { PluginSetupContract } from '../../../plugins/encrypted_saved_objects/server'; +import { EncryptedSavedObjectsPluginSetup } from '../../../plugins/encrypted_saved_objects/server'; // @ts-ignore import { AuditLogger } from '../../server/lib/audit_logger'; @@ -29,7 +29,7 @@ export const encryptedSavedObjects = (kibana: { init(server: Legacy.Server) { const encryptedSavedObjectsPlugin = (server.newPlatform.setup.plugins - .encryptedSavedObjects as unknown) as PluginSetupContract; + .encryptedSavedObjects as unknown) as EncryptedSavedObjectsPluginSetup; if (!encryptedSavedObjectsPlugin) { throw new Error('New Platform XPack EncryptedSavedObjects plugin is not available.'); } diff --git a/x-pack/legacy/plugins/reporting/index.ts b/x-pack/legacy/plugins/reporting/index.ts index d2a68e309a4b37..966e4ff209ad68 100644 --- a/x-pack/legacy/plugins/reporting/index.ts +++ b/x-pack/legacy/plugins/reporting/index.ts @@ -9,7 +9,7 @@ import { Legacy } from 'kibana'; import { IUiSettingsClient } from 'kibana/server'; import { resolve } from 'path'; import { PluginStart as DataPluginStart } from '../../../../src/plugins/data/server'; -import { PluginSetupContract as SecurityPluginSetup } from '../../../plugins/security/server'; +import { SecurityPluginSetup } from '../../../plugins/security/server'; import { PLUGIN_ID, UI_SETTINGS_CUSTOM_PDF_LOGO } from './common/constants'; import { config as reportingConfig } from './config'; import { LegacySetup, ReportingPlugin, reportingPluginFactory } from './server/plugin'; diff --git a/x-pack/legacy/plugins/reporting/server/plugin.ts b/x-pack/legacy/plugins/reporting/server/plugin.ts index a2938d442f7df0..e618d23e8ed1f9 100644 --- a/x-pack/legacy/plugins/reporting/server/plugin.ts +++ b/x-pack/legacy/plugins/reporting/server/plugin.ts @@ -14,7 +14,7 @@ import { } from 'src/core/server'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { PluginStart as DataPluginStart } from '../../../../../src/plugins/data/server'; -import { PluginSetupContract as SecurityPluginSetup } from '../../../../plugins/security/server'; +import { SecurityPluginSetup } from '../../../../plugins/security/server'; // @ts-ignore import { mirrorPluginStatus } from '../../../server/lib/mirror_plugin_status'; import { XPackMainPlugin } from '../../xpack_main/server/xpack_main'; diff --git a/x-pack/legacy/plugins/siem/server/plugin.ts b/x-pack/legacy/plugins/siem/server/plugin.ts index 96eef2f44e5a06..94314367be59cf 100644 --- a/x-pack/legacy/plugins/siem/server/plugin.ts +++ b/x-pack/legacy/plugins/siem/server/plugin.ts @@ -6,7 +6,7 @@ import { i18n } from '@kbn/i18n'; import { CoreSetup, PluginInitializerContext, Logger } from 'src/core/server'; -import { PluginSetupContract as SecurityPlugin } from '../../../../plugins/security/server'; +import { SecurityPluginSetup } from '../../../../plugins/security/server'; import { PluginSetupContract as FeaturesSetupContract } from '../../../../plugins/features/server'; import { initServer } from './init_server'; import { compose } from './lib/compose/kibana'; @@ -17,7 +17,7 @@ import { ruleStatusSavedObjectType, } from './saved_objects'; -export type SiemPluginSecurity = Pick; +export type SiemPluginSecurity = Pick; export interface PluginsSetup { features: FeaturesSetupContract; diff --git a/x-pack/plugins/actions/server/lib/action_executor.ts b/x-pack/plugins/actions/server/lib/action_executor.ts index be6916a74fe88d..03a892a42792e2 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.ts @@ -12,7 +12,7 @@ import { GetServicesFunction, RawAction, } from '../types'; -import { PluginStartContract as EncryptedSavedObjectsStartContract } from '../../../encrypted_saved_objects/server'; +import { EncryptedSavedObjectsPluginStart } from '../../../encrypted_saved_objects/server'; import { SpacesServiceSetup } from '../../../spaces/server'; import { EVENT_LOG_ACTIONS } from '../plugin'; import { IEvent, IEventLogger } from '../../../event_log/server'; @@ -21,7 +21,7 @@ export interface ActionExecutorContext { logger: Logger; spaces?: SpacesServiceSetup; getServices: GetServicesFunction; - encryptedSavedObjectsPlugin: EncryptedSavedObjectsStartContract; + encryptedSavedObjectsPlugin: EncryptedSavedObjectsPluginStart; actionTypeRegistry: ActionTypeRegistryContract; eventLogger: IEventLogger; } diff --git a/x-pack/plugins/actions/server/lib/task_runner_factory.ts b/x-pack/plugins/actions/server/lib/task_runner_factory.ts index c3e89e0c16efcc..c78b43f4ef3ba3 100644 --- a/x-pack/plugins/actions/server/lib/task_runner_factory.ts +++ b/x-pack/plugins/actions/server/lib/task_runner_factory.ts @@ -8,12 +8,12 @@ import { ActionExecutorContract } from './action_executor'; import { ExecutorError } from './executor_error'; import { Logger, CoreStart } from '../../../../../src/core/server'; import { RunContext } from '../../../task_manager/server'; -import { PluginStartContract as EncryptedSavedObjectsStartContract } from '../../../encrypted_saved_objects/server'; +import { EncryptedSavedObjectsPluginStart } from '../../../encrypted_saved_objects/server'; import { ActionTaskParams, GetBasePathFunction, SpaceIdToNamespaceFunction } from '../types'; export interface TaskRunnerContext { logger: Logger; - encryptedSavedObjectsPlugin: EncryptedSavedObjectsStartContract; + encryptedSavedObjectsPlugin: EncryptedSavedObjectsPluginStart; spaceIdToNamespace: SpaceIdToNamespaceFunction; getBasePath: GetBasePathFunction; getScopedSavedObjectsClient: CoreStart['savedObjects']['getScopedClient']; diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index cb0e3347541fd7..dab09fc455ecf6 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -20,8 +20,8 @@ import { } from '../../../../src/core/server'; import { - PluginSetupContract as EncryptedSavedObjectsSetupContract, - PluginStartContract as EncryptedSavedObjectsStartContract, + EncryptedSavedObjectsPluginSetup, + EncryptedSavedObjectsPluginStart, } from '../../encrypted_saved_objects/server'; import { TaskManagerSetupContract, TaskManagerStartContract } from '../../task_manager/server'; import { LicensingPluginSetup } from '../../licensing/server'; @@ -67,13 +67,13 @@ export interface PluginStartContract { export interface ActionsPluginsSetup { taskManager: TaskManagerSetupContract; - encryptedSavedObjects: EncryptedSavedObjectsSetupContract; + encryptedSavedObjects: EncryptedSavedObjectsPluginSetup; licensing: LicensingPluginSetup; spaces?: SpacesPluginSetup; event_log: IEventLogService; } export interface ActionsPluginsStart { - encryptedSavedObjects: EncryptedSavedObjectsStartContract; + encryptedSavedObjects: EncryptedSavedObjectsPluginStart; taskManager: TaskManagerStartContract; } diff --git a/x-pack/plugins/case/server/plugin.ts b/x-pack/plugins/case/server/plugin.ts index c52461cade0586..37d087433a2ed1 100644 --- a/x-pack/plugins/case/server/plugin.ts +++ b/x-pack/plugins/case/server/plugin.ts @@ -9,7 +9,7 @@ import { CoreSetup, Logger, PluginInitializerContext } from 'kibana/server'; import { ConfigType } from './config'; import { initCaseApi } from './routes/api'; import { CaseService } from './services'; -import { PluginSetupContract as SecurityPluginSetup } from '../../security/server'; +import { SecurityPluginSetup } from '../../security/server'; function createConfig$(context: PluginInitializerContext) { return context.config.create().pipe(map(config => config)); diff --git a/x-pack/plugins/case/server/services/index.ts b/x-pack/plugins/case/server/services/index.ts index 684d905a5c71fe..531d5fa5b87e5b 100644 --- a/x-pack/plugins/case/server/services/index.ts +++ b/x-pack/plugins/case/server/services/index.ts @@ -21,10 +21,7 @@ import { UpdatedCaseType, UpdatedCommentType, } from '../routes/api/types'; -import { - AuthenticatedUser, - PluginSetupContract as SecurityPluginSetup, -} from '../../../security/server'; +import { AuthenticatedUser, SecurityPluginSetup } from '../../../security/server'; interface ClientArgs { client: SavedObjectsClientContract; diff --git a/x-pack/plugins/encrypted_saved_objects/server/index.ts b/x-pack/plugins/encrypted_saved_objects/server/index.ts index 5e6edb95ec37a6..3b4b91de355c74 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/index.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/index.ts @@ -9,7 +9,7 @@ import { ConfigSchema } from './config'; import { Plugin } from './plugin'; export { EncryptedSavedObjectTypeRegistration, EncryptionError } from './crypto'; -export { PluginSetupContract, PluginStartContract } from './plugin'; +export { EncryptedSavedObjectsPluginSetup, EncryptedSavedObjectsPluginStart } from './plugin'; export const config = { schema: ConfigSchema }; export const plugin = (initializerContext: PluginInitializerContext) => diff --git a/x-pack/plugins/encrypted_saved_objects/server/mocks.ts b/x-pack/plugins/encrypted_saved_objects/server/mocks.ts index 7f53f47760f129..13d7127db78353 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/mocks.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/mocks.ts @@ -4,21 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PluginSetupContract, PluginStartContract } from './plugin'; +import { EncryptedSavedObjectsPluginSetup, EncryptedSavedObjectsPluginStart } from './plugin'; function createEncryptedSavedObjectsSetupMock() { return { registerType: jest.fn(), __legacyCompat: { registerLegacyAPI: jest.fn() }, usingEphemeralEncryptionKey: true, - } as jest.Mocked; + } as jest.Mocked; } function createEncryptedSavedObjectsStartMock() { return { isEncryptionError: jest.fn(), getDecryptedAsInternalUser: jest.fn(), - } as jest.Mocked; + } as jest.Mocked; } export const encryptedSavedObjectsMock = { diff --git a/x-pack/plugins/encrypted_saved_objects/server/plugin.ts b/x-pack/plugins/encrypted_saved_objects/server/plugin.ts index d9185251ca4664..a0218c51c2723c 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/plugin.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/plugin.ts @@ -20,13 +20,13 @@ import { import { EncryptedSavedObjectsAuditLogger } from './audit'; import { SavedObjectsSetup, setupSavedObjects } from './saved_objects'; -export interface PluginSetupContract { +export interface EncryptedSavedObjectsPluginSetup { registerType: (typeRegistration: EncryptedSavedObjectTypeRegistration) => void; __legacyCompat: { registerLegacyAPI: (legacyAPI: LegacyAPI) => void }; usingEphemeralEncryptionKey: boolean; } -export interface PluginStartContract extends SavedObjectsSetup { +export interface EncryptedSavedObjectsPluginStart extends SavedObjectsSetup { isEncryptionError: (error: Error) => boolean; } @@ -59,7 +59,7 @@ export class Plugin { this.logger = this.initializerContext.logger.get(); } - public async setup(core: CoreSetup): Promise { + public async setup(core: CoreSetup): Promise { const { config, usingEphemeralEncryptionKey } = await createConfig$(this.initializerContext) .pipe(first()) .toPromise(); diff --git a/x-pack/plugins/security/server/index.ts b/x-pack/plugins/security/server/index.ts index 17e49b8cf40d30..c0e86b289fe545 100644 --- a/x-pack/plugins/security/server/index.ts +++ b/x-pack/plugins/security/server/index.ts @@ -12,7 +12,7 @@ import { RecursiveReadonly, } from '../../../../src/core/server'; import { ConfigSchema } from './config'; -import { Plugin, PluginSetupContract, PluginSetupDependencies } from './plugin'; +import { Plugin, SecurityPluginSetup, PluginSetupDependencies } from './plugin'; // These exports are part of public Security plugin contract, any change in signature of exported // functions or removal of exports should be considered as a breaking change. @@ -24,7 +24,7 @@ export { InvalidateAPIKeyParams, InvalidateAPIKeyResult, } from './authentication'; -export { PluginSetupContract }; +export { SecurityPluginSetup }; export { AuthenticatedUser } from '../common/model'; export const config: PluginConfigDescriptor> = { @@ -35,7 +35,7 @@ export const config: PluginConfigDescriptor> = { ], }; export const plugin: PluginInitializer< - RecursiveReadonly, + RecursiveReadonly, void, PluginSetupDependencies > = (initializerContext: PluginInitializerContext) => new Plugin(initializerContext); diff --git a/x-pack/plugins/security/server/mocks.ts b/x-pack/plugins/security/server/mocks.ts index d5c08d5ab1ab94..ababf12c2be606 100644 --- a/x-pack/plugins/security/server/mocks.ts +++ b/x-pack/plugins/security/server/mocks.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PluginSetupContract } from './plugin'; +import { SecurityPluginSetup } from './plugin'; import { authenticationMock } from './authentication/index.mock'; import { authorizationMock } from './authorization/index.mock'; @@ -19,7 +19,7 @@ function createSetupMock() { mode: mockAuthz.mode, }, registerSpacesService: jest.fn(), - __legacyCompat: {} as PluginSetupContract['__legacyCompat'], + __legacyCompat: {} as SecurityPluginSetup['__legacyCompat'], }; } diff --git a/x-pack/plugins/security/server/plugin.ts b/x-pack/plugins/security/server/plugin.ts index ce682d8b30eb76..57644182347398 100644 --- a/x-pack/plugins/security/server/plugin.ts +++ b/x-pack/plugins/security/server/plugin.ts @@ -49,7 +49,7 @@ export interface LegacyAPI { /** * Describes public Security plugin contract returned at the `setup` stage. */ -export interface PluginSetupContract { +export interface SecurityPluginSetup { authc: Authentication; authz: Pick; @@ -166,7 +166,7 @@ export class Plugin { csp: core.http.csp, }); - return deepFreeze({ + return deepFreeze({ authc, authz: { diff --git a/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.test.ts b/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.test.ts index 24a994e836e87d..74e75fb8f12c79 100644 --- a/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.test.ts +++ b/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PluginSetupContract as SecuritySetupContract } from '../../../../security/server'; +import { SecurityPluginSetup } from '../../../../security/server'; import { SpacesClient } from './spaces_client'; import { ConfigType, ConfigSchema } from '../../config'; import { GetSpacePurpose } from '../../../common/model/types'; @@ -224,17 +224,17 @@ describe('#getAll', () => { [ { purpose: undefined, - expectedPrivilege: (mockAuthorization: SecuritySetupContract['authz']) => + expectedPrivilege: (mockAuthorization: SecurityPluginSetup['authz']) => mockAuthorization.actions.login, }, { purpose: 'any', - expectedPrivilege: (mockAuthorization: SecuritySetupContract['authz']) => + expectedPrivilege: (mockAuthorization: SecurityPluginSetup['authz']) => mockAuthorization.actions.login, }, { purpose: 'copySavedObjectsIntoSpace', - expectedPrivilege: (mockAuthorization: SecuritySetupContract['authz']) => + expectedPrivilege: (mockAuthorization: SecurityPluginSetup['authz']) => mockAuthorization.actions.ui.get('savedObjectsManagement', 'copyIntoSpace'), }, ].forEach(scenario => { diff --git a/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.ts b/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.ts index f964ae7d7ac32d..22c34c03368e36 100644 --- a/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.ts +++ b/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.ts @@ -6,7 +6,7 @@ import Boom from 'boom'; import { omit } from 'lodash'; import { KibanaRequest } from 'src/core/server'; -import { PluginSetupContract as SecurityPluginSetupContract } from '../../../../security/server'; +import { SecurityPluginSetup } from '../../../../security/server'; import { isReservedSpace } from '../../../common/is_reserved_space'; import { Space } from '../../../common/model/space'; import { SpacesAuditLogger } from '../audit_logger'; @@ -17,7 +17,7 @@ const SUPPORTED_GET_SPACE_PURPOSES: GetSpacePurpose[] = ['any', 'copySavedObject const PURPOSE_PRIVILEGE_MAP: Record< GetSpacePurpose, - (authorization: SecurityPluginSetupContract['authz']) => string + (authorization: SecurityPluginSetup['authz']) => string > = { any: authorization => authorization.actions.login, copySavedObjectsIntoSpace: authorization => @@ -28,7 +28,7 @@ export class SpacesClient { constructor( private readonly auditLogger: SpacesAuditLogger, private readonly debugLogger: (message: string) => void, - private readonly authorization: SecurityPluginSetupContract['authz'] | null, + private readonly authorization: SecurityPluginSetup['authz'] | null, private readonly callWithRequestSavedObjectRepository: any, private readonly config: ConfigType, private readonly internalSavedObjectRepository: any, diff --git a/x-pack/plugins/spaces/server/plugin.ts b/x-pack/plugins/spaces/server/plugin.ts index b8ef81c05f7aa4..52ff7eaee3d68c 100644 --- a/x-pack/plugins/spaces/server/plugin.ts +++ b/x-pack/plugins/spaces/server/plugin.ts @@ -14,7 +14,7 @@ import { PluginInitializerContext, } from '../../../../src/core/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; -import { PluginSetupContract as SecurityPluginSetup } from '../../security/server'; +import { SecurityPluginSetup } from '../../security/server'; import { LicensingPluginSetup } from '../../licensing/server'; import { XPackMainPlugin } from '../../../legacy/plugins/xpack_main/server/xpack_main'; import { createDefaultSpace } from './lib/create_default_space'; diff --git a/x-pack/plugins/spaces/server/spaces_service/spaces_service.ts b/x-pack/plugins/spaces/server/spaces_service/spaces_service.ts index f8ed58fa575518..95bda96d894615 100644 --- a/x-pack/plugins/spaces/server/spaces_service/spaces_service.ts +++ b/x-pack/plugins/spaces/server/spaces_service/spaces_service.ts @@ -8,7 +8,7 @@ import { map, take } from 'rxjs/operators'; import { Observable, Subscription } from 'rxjs'; import { Legacy } from 'kibana'; import { Logger, KibanaRequest, CoreSetup } from '../../../../../src/core/server'; -import { PluginSetupContract as SecurityPluginSetup } from '../../../security/server'; +import { SecurityPluginSetup } from '../../../security/server'; import { LegacyAPI } from '../plugin'; import { SpacesClient } from '../lib/spaces_client'; import { ConfigType } from '../config'; diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/aad/index.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/aad/index.ts index d7bee93f5c94b9..7194c642e7015e 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/aad/index.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/aad/index.ts @@ -8,7 +8,7 @@ import Joi from 'joi'; import Hapi from 'hapi'; import { Legacy } from 'kibana'; import KbnServer from '../../../../../../../src/legacy/server/kbn_server'; -import { PluginStartContract } from '../../../../../../plugins/encrypted_saved_objects/server'; +import { EncryptedSavedObjectsPluginStart } from '../../../../../../plugins/encrypted_saved_objects/server'; interface CheckAADRequest extends Hapi.Request { payload: { @@ -25,7 +25,8 @@ export default function(kibana: any) { name: 'aad-fixtures', init(server: Legacy.Server) { const newPlatform = ((server as unknown) as KbnServer).newPlatform; - const esoPlugin = newPlatform.start.plugins.encryptedSavedObjects as PluginStartContract; + const esoPlugin = newPlatform.start.plugins + .encryptedSavedObjects as EncryptedSavedObjectsPluginStart; server.route({ method: 'POST', diff --git a/x-pack/test/plugin_api_integration/plugins/encrypted_saved_objects/index.ts b/x-pack/test/plugin_api_integration/plugins/encrypted_saved_objects/index.ts index a194e477da7559..e61b8f24a1f69b 100644 --- a/x-pack/test/plugin_api_integration/plugins/encrypted_saved_objects/index.ts +++ b/x-pack/test/plugin_api_integration/plugins/encrypted_saved_objects/index.ts @@ -8,8 +8,8 @@ import { Request } from 'hapi'; import { boomify, badRequest } from 'boom'; import { Legacy } from 'kibana'; import { - PluginSetupContract, - PluginStartContract, + EncryptedSavedObjectsPluginSetup, + EncryptedSavedObjectsPluginStart, } from '../../../../plugins/encrypted_saved_objects/server'; const SAVED_OBJECT_WITH_SECRET_TYPE = 'saved-object-with-secret'; @@ -26,7 +26,7 @@ export default function esoPlugin(kibana: any) { path: '/api/saved_objects/get-decrypted-as-internal-user/{id}', async handler(request: Request) { const encryptedSavedObjectsStart = server.newPlatform.start.plugins - .encryptedSavedObjects as PluginStartContract; + .encryptedSavedObjects as EncryptedSavedObjectsPluginStart; const namespace = server.plugins.spaces && server.plugins.spaces.getSpaceId(request); try { return await encryptedSavedObjectsStart.getDecryptedAsInternalUser( @@ -44,7 +44,8 @@ export default function esoPlugin(kibana: any) { }, }); - (server.newPlatform.setup.plugins.encryptedSavedObjects as PluginSetupContract).registerType({ + (server.newPlatform.setup.plugins + .encryptedSavedObjects as EncryptedSavedObjectsPluginSetup).registerType({ type: SAVED_OBJECT_WITH_SECRET_TYPE, attributesToEncrypt: new Set(['privateProperty']), attributesToExcludeFromAAD: new Set(['publicPropertyExcludedFromAAD']), From f2713659f63fda25881e1c99087dc084cf4c85a1 Mon Sep 17 00:00:00 2001 From: Peter Pisljar Date: Mon, 3 Feb 2020 09:01:34 -0500 Subject: [PATCH 26/33] filtering (making it work with expressions) (#55351) * adding meta information to KibanaDatatable * updating filtering functions to use new information * moving filter creation to APPLY_FILTER_ACTION * adding SELECT_RANGE_ACTION and TRIGGER * making _meta optional * inlining legacy code for inspector * fixing jest tests * keeping apply_filter_action and adding value_click_action and trigger * utilities for serializing/unserializing aggConfigs * renaming prop to indexPatternId * cleanup * updating interpreter functional baselines * trying to fix tests * Fix legend tests * reverting update to multi metric screenshot * updating based on review * updating tests Co-authored-by: Nick Partridge Co-authored-by: Elastic Machine --- .../public/actions}/filters/brush_event.js | 14 +- .../actions}/filters/brush_event.test.js | 78 +++++++---- .../filters/brush_event.test.mocks.ts | 2 +- .../filters/create_filters_from_event.js} | 51 ++++--- .../public/actions/select_range_action.ts | 91 +++++++++++++ .../data/public/actions/value_click_action.ts | 126 ++++++++++++++++++ src/legacy/core_plugins/data/public/legacy.ts | 6 +- src/legacy/core_plugins/data/public/plugin.ts | 28 +++- .../build_tabular_inspector_data.ts | 4 +- .../search/expressions/create_filter.js | 66 +++++++++ .../data/public/search/expressions/esaggs.ts | 2 + .../data/public/search/expressions/utils.ts | 51 +++++++ .../core_plugins/data/public/search/index.ts | 1 + .../vislib/components/legend/legend.test.tsx | 58 ++++---- .../vislib/components/legend/legend.tsx | 60 +++++---- .../public/embeddable/visualize_embeddable.ts | 36 ++--- .../public/np_ready/public/filters/index.ts | 21 --- .../public/np_ready/public/index.ts | 3 - .../np_ready/public/types/base_vis_type.js | 10 -- src/legacy/ui/public/agg_types/agg_config.ts | 2 +- src/legacy/ui/public/agg_types/agg_types.ts | 92 +++++++++++++ src/legacy/ui/public/agg_types/index.ts | 80 +---------- .../ui/public/agg_types/param_types/field.ts | 4 +- src/legacy/ui/public/agg_types/utils.ts | 2 +- .../public/vis/lib/least_common_interval.ts | 2 +- src/plugins/embeddable/public/bootstrap.ts | 16 +++ src/plugins/embeddable/public/index.ts | 2 + .../embeddable/public/lib/triggers/index.ts | 2 + .../expression_types/kibana_datatable.ts | 7 + .../screenshots/baseline/combined_test.png | Bin 22801 -> 16994 bytes .../baseline/final_screenshot_test.png | Bin 22852 -> 17033 bytes .../screenshots/baseline/metric_all_data.png | Bin 32900 -> 24240 bytes .../baseline/metric_invalid_data.png | Bin 1920 -> 1806 bytes .../baseline/metric_percentage_mode.png | Bin 32587 -> 23705 bytes .../baseline/metric_single_metric_data.png | Bin 29643 -> 22004 bytes .../screenshots/baseline/partial_test_1.png | Bin 15140 -> 11228 bytes .../screenshots/baseline/partial_test_2.png | Bin 22801 -> 16994 bytes .../screenshots/baseline/partial_test_3.png | Bin 7049 -> 7054 bytes .../baseline/tagcloud_all_data.png | Bin 15701 -> 11827 bytes .../baseline/tagcloud_fontsize.png | Bin 13808 -> 10314 bytes .../baseline/tagcloud_invalid_data.png | Bin 1920 -> 1806 bytes .../baseline/tagcloud_metric_data.png | Bin 9163 -> 6712 bytes .../screenshots/baseline/tagcloud_options.png | Bin 18998 -> 15100 bytes .../snapshots/baseline/combined_test2.json | 2 +- .../snapshots/baseline/combined_test3.json | 2 +- .../snapshots/baseline/final_output_test.json | 2 +- .../snapshots/baseline/metric_all_data.json | 2 +- .../baseline/metric_multi_metric_data.json | 2 +- .../baseline/metric_percentage_mode.json | 2 +- .../baseline/metric_single_metric_data.json | 2 +- .../snapshots/baseline/partial_test_1.json | 2 +- .../snapshots/baseline/partial_test_2.json | 2 +- .../snapshots/baseline/partial_test_3.json | 2 +- .../snapshots/baseline/step_output_test2.json | 2 +- .../snapshots/baseline/step_output_test3.json | 2 +- .../snapshots/baseline/tagcloud_all_data.json | 2 +- .../snapshots/baseline/tagcloud_fontsize.json | 2 +- .../baseline/tagcloud_metric_data.json | 2 +- .../snapshots/baseline/tagcloud_options.json | 2 +- .../snapshots/session/combined_test2.json | 2 +- .../snapshots/session/combined_test3.json | 2 +- .../snapshots/session/final_output_test.json | 2 +- .../snapshots/session/metric_all_data.json | 2 +- .../session/metric_multi_metric_data.json | 2 +- .../session/metric_percentage_mode.json | 2 +- .../session/metric_single_metric_data.json | 2 +- .../snapshots/session/partial_test_1.json | 2 +- .../snapshots/session/partial_test_2.json | 2 +- .../snapshots/session/partial_test_3.json | 2 +- .../snapshots/session/step_output_test2.json | 2 +- .../snapshots/session/step_output_test3.json | 2 +- .../snapshots/session/tagcloud_all_data.json | 2 +- .../snapshots/session/tagcloud_fontsize.json | 2 +- .../session/tagcloud_metric_data.json | 2 +- .../snapshots/session/tagcloud_options.json | 2 +- 75 files changed, 700 insertions(+), 281 deletions(-) rename src/legacy/core_plugins/{visualizations/public/np_ready/public => data/public/actions}/filters/brush_event.js (81%) rename src/legacy/core_plugins/{visualizations/public/np_ready/public => data/public/actions}/filters/brush_event.test.js (71%) rename src/legacy/core_plugins/{visualizations/public/np_ready/public => data/public/actions}/filters/brush_event.test.mocks.ts (92%) rename src/legacy/core_plugins/{visualizations/public/np_ready/public/filters/vis_filters.js => data/public/actions/filters/create_filters_from_event.js} (72%) create mode 100644 src/legacy/core_plugins/data/public/actions/select_range_action.ts create mode 100644 src/legacy/core_plugins/data/public/actions/value_click_action.ts create mode 100644 src/legacy/core_plugins/data/public/search/expressions/create_filter.js create mode 100644 src/legacy/core_plugins/data/public/search/expressions/utils.ts delete mode 100644 src/legacy/core_plugins/visualizations/public/np_ready/public/filters/index.ts create mode 100644 src/legacy/ui/public/agg_types/agg_types.ts diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/filters/brush_event.js b/src/legacy/core_plugins/data/public/actions/filters/brush_event.js similarity index 81% rename from src/legacy/core_plugins/visualizations/public/np_ready/public/filters/brush_event.js rename to src/legacy/core_plugins/data/public/actions/filters/brush_event.js index e0854205b132ea..67711bd4599a2d 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/filters/brush_event.js +++ b/src/legacy/core_plugins/data/public/actions/filters/brush_event.js @@ -19,9 +19,10 @@ import _ from 'lodash'; import moment from 'moment'; -import { esFilters } from '../../../../../../../plugins/data/public'; +import { esFilters } from '../../../../../../plugins/data/public'; +import { deserializeAggConfig } from '../../search/expressions/utils'; -export function onBrushEvent(event) { +export async function onBrushEvent(event, getIndexPatterns) { const isNumber = event.data.ordered; const isDate = isNumber && event.data.ordered.date; @@ -29,9 +30,12 @@ export function onBrushEvent(event) { if (!xRaw) return []; const column = xRaw.table.columns[xRaw.column]; if (!column) return []; - const aggConfig = event.aggConfigs[xRaw.column]; - if (!aggConfig) return []; - const indexPattern = aggConfig.getIndexPattern(); + if (!column.meta) return []; + const indexPattern = await getIndexPatterns().get(column.meta.indexPatternId); + const aggConfig = deserializeAggConfig({ + ...column.meta, + indexPattern, + }); const field = aggConfig.params.field; if (!field) return []; diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/filters/brush_event.test.js b/src/legacy/core_plugins/data/public/actions/filters/brush_event.test.js similarity index 71% rename from src/legacy/core_plugins/visualizations/public/np_ready/public/filters/brush_event.test.js rename to src/legacy/core_plugins/data/public/actions/filters/brush_event.test.js index 215d440edd9d02..a6fe58503cd021 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/filters/brush_event.test.js +++ b/src/legacy/core_plugins/data/public/actions/filters/brush_event.test.js @@ -20,21 +20,36 @@ import _ from 'lodash'; import moment from 'moment'; import expect from '@kbn/expect'; + +jest.mock('../../../../../ui/public/agg_types/agg_configs', () => ({ + AggConfigs: function AggConfigs() { + return { + createAggConfig: ({ params }) => ({ + params, + getIndexPattern: () => ({ + timeFieldName: 'time', + }), + }), + }; + }, +})); + import { onBrushEvent } from './brush_event'; describe('brushEvent', () => { const DAY_IN_MS = 24 * 60 * 60 * 1000; const JAN_01_2014 = 1388559600000; + const aggConfigs = [ + { + params: {}, + getIndexPattern: () => ({ + timeFieldName: 'time', + }), + }, + ]; + const baseEvent = { - aggConfigs: [ - { - params: {}, - getIndexPattern: () => ({ - timeFieldName: 'time', - }), - }, - ], data: { fieldFormatter: _.constant({}), series: [ @@ -47,6 +62,11 @@ describe('brushEvent', () => { columns: [ { id: '1', + meta: { + type: 'histogram', + indexPatternId: 'indexPatternId', + aggConfigParams: aggConfigs[0].params, + }, }, ], }, @@ -69,9 +89,11 @@ describe('brushEvent', () => { expect(onBrushEvent).to.be.a(Function); }); - test('ignores event when data.xAxisField not provided', () => { + test('ignores event when data.xAxisField not provided', async () => { const event = _.cloneDeep(baseEvent); - const filters = onBrushEvent(event); + const filters = await onBrushEvent(event, () => ({ + get: () => baseEvent.data.indexPattern, + })); expect(filters.length).to.equal(0); }); @@ -84,22 +106,26 @@ describe('brushEvent', () => { }; beforeEach(() => { + aggConfigs[0].params.field = dateField; dateEvent = _.cloneDeep(baseEvent); - dateEvent.aggConfigs[0].params.field = dateField; dateEvent.data.ordered = { date: true }; }); - test('by ignoring the event when range spans zero time', () => { + test('by ignoring the event when range spans zero time', async () => { const event = _.cloneDeep(dateEvent); event.range = [JAN_01_2014, JAN_01_2014]; - const filters = onBrushEvent(event); + const filters = await onBrushEvent(event, () => ({ + get: () => dateEvent.data.indexPattern, + })); expect(filters.length).to.equal(0); }); - test('by updating the timefilter', () => { + test('by updating the timefilter', async () => { const event = _.cloneDeep(dateEvent); event.range = [JAN_01_2014, JAN_01_2014 + DAY_IN_MS]; - const filters = onBrushEvent(event); + const filters = await onBrushEvent(event, () => ({ + get: async () => dateEvent.data.indexPattern, + })); expect(filters[0].range.time.gte).to.be(new Date(JAN_01_2014).toISOString()); // Set to a baseline timezone for comparison. expect(filters[0].range.time.lt).to.be(new Date(JAN_01_2014 + DAY_IN_MS).toISOString()); @@ -114,17 +140,19 @@ describe('brushEvent', () => { }; beforeEach(() => { + aggConfigs[0].params.field = dateField; dateEvent = _.cloneDeep(baseEvent); - dateEvent.aggConfigs[0].params.field = dateField; dateEvent.data.ordered = { date: true }; }); - test('creates a new range filter', () => { + test('creates a new range filter', async () => { const event = _.cloneDeep(dateEvent); const rangeBegin = JAN_01_2014; const rangeEnd = rangeBegin + DAY_IN_MS; event.range = [rangeBegin, rangeEnd]; - const filters = onBrushEvent(event); + const filters = await onBrushEvent(event, () => ({ + get: () => dateEvent.data.indexPattern, + })); expect(filters.length).to.equal(1); expect(filters[0].range.anotherTimeField.gte).to.equal(moment(rangeBegin).toISOString()); expect(filters[0].range.anotherTimeField.lt).to.equal(moment(rangeEnd).toISOString()); @@ -142,22 +170,26 @@ describe('brushEvent', () => { }; beforeEach(() => { + aggConfigs[0].params.field = numberField; numberEvent = _.cloneDeep(baseEvent); - numberEvent.aggConfigs[0].params.field = numberField; numberEvent.data.ordered = { date: false }; }); - test('by ignoring the event when range does not span at least 2 values', () => { + test('by ignoring the event when range does not span at least 2 values', async () => { const event = _.cloneDeep(numberEvent); event.range = [1]; - const filters = onBrushEvent(event); + const filters = await onBrushEvent(event, () => ({ + get: () => numberEvent.data.indexPattern, + })); expect(filters.length).to.equal(0); }); - test('by creating a new filter', () => { + test('by creating a new filter', async () => { const event = _.cloneDeep(numberEvent); event.range = [1, 2, 3, 4]; - const filters = onBrushEvent(event); + const filters = await onBrushEvent(event, () => ({ + get: () => numberEvent.data.indexPattern, + })); expect(filters.length).to.equal(1); expect(filters[0].range.numberField.gte).to.equal(1); expect(filters[0].range.numberField.lt).to.equal(4); diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/filters/brush_event.test.mocks.ts b/src/legacy/core_plugins/data/public/actions/filters/brush_event.test.mocks.ts similarity index 92% rename from src/legacy/core_plugins/visualizations/public/np_ready/public/filters/brush_event.test.mocks.ts rename to src/legacy/core_plugins/data/public/actions/filters/brush_event.test.mocks.ts index f0de2f88dcb82c..2cecfd0fe8b767 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/filters/brush_event.test.mocks.ts +++ b/src/legacy/core_plugins/data/public/actions/filters/brush_event.test.mocks.ts @@ -17,7 +17,7 @@ * under the License. */ -import { chromeServiceMock } from '../../../../../../../core/public/mocks'; +import { chromeServiceMock } from '../../../../../../core/public/mocks'; jest.doMock('ui/new_platform', () => ({ npStart: { diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/filters/vis_filters.js b/src/legacy/core_plugins/data/public/actions/filters/create_filters_from_event.js similarity index 72% rename from src/legacy/core_plugins/visualizations/public/np_ready/public/filters/vis_filters.js rename to src/legacy/core_plugins/data/public/actions/filters/create_filters_from_event.js index 303dec690e62bb..1037c718d0003f 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/filters/vis_filters.js +++ b/src/legacy/core_plugins/data/public/actions/filters/create_filters_from_event.js @@ -17,8 +17,10 @@ * under the License. */ -import { onBrushEvent } from './brush_event'; -import { esFilters } from '../../../../../../../plugins/data/public'; +import { esFilters } from '../../../../../../plugins/data/public'; +import { deserializeAggConfig } from '../../search/expressions/utils'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { getIndexPatterns } from '../../../../../../plugins/data/public/services'; /** * For terms aggregations on `__other__` buckets, this assembles a list of applicable filter @@ -63,11 +65,16 @@ const getOtherBucketFilterTerms = (table, columnIndex, rowIndex) => { * @param {string} cellValue - value of the current cell * @return {array|string} - filter or list of filters to provide to queryFilter.addFilters() */ -const createFilter = (aggConfigs, table, columnIndex, rowIndex, cellValue) => { +const createFilter = async (table, columnIndex, rowIndex) => { + if (!table || !table.columns || !table.columns[columnIndex]) return; const column = table.columns[columnIndex]; - const aggConfig = aggConfigs[columnIndex]; + const aggConfig = deserializeAggConfig({ + type: column.meta.type, + aggConfigParams: column.meta.aggConfigParams, + indexPattern: await getIndexPatterns().get(column.meta.indexPatternId), + }); let filter = []; - const value = rowIndex > -1 ? table.rows[rowIndex][column.id] : cellValue; + const value = rowIndex > -1 ? table.rows[rowIndex][column.id] : null; if (value === null || value === undefined || !aggConfig.isFilterable()) { return; } @@ -85,26 +92,28 @@ const createFilter = (aggConfigs, table, columnIndex, rowIndex, cellValue) => { return filter; }; -const createFiltersFromEvent = event => { +const createFiltersFromEvent = async event => { const filters = []; const dataPoints = event.data || [event]; - dataPoints - .filter(point => point) - .forEach(val => { - const { table, column, row, value } = val; - const filter = createFilter(event.aggConfigs, table, column, row, value); - if (filter) { - filter.forEach(f => { - if (event.negate) { - f = esFilters.toggleFilterNegated(f); - } - filters.push(f); - }); - } - }); + await Promise.all( + dataPoints + .filter(point => point) + .map(async val => { + const { table, column, row } = val; + const filter = await createFilter(table, column, row); + if (filter) { + filter.forEach(f => { + if (event.negate) { + f = esFilters.toggleFilterNegated(f); + } + filters.push(f); + }); + } + }) + ); return filters; }; -export { createFilter, createFiltersFromEvent, onBrushEvent }; +export { createFilter, createFiltersFromEvent }; diff --git a/src/legacy/core_plugins/data/public/actions/select_range_action.ts b/src/legacy/core_plugins/data/public/actions/select_range_action.ts new file mode 100644 index 00000000000000..4ea5c78a9fd2b4 --- /dev/null +++ b/src/legacy/core_plugins/data/public/actions/select_range_action.ts @@ -0,0 +1,91 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { + IAction, + createAction, + IncompatibleActionError, +} from '../../../../../plugins/ui_actions/public'; +// @ts-ignore +import { onBrushEvent } from './filters/brush_event'; +import { + esFilters, + FilterManager, + TimefilterContract, + changeTimeFilter, + extractTimeFilter, + mapAndFlattenFilters, +} from '../../../../../plugins/data/public'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { getIndexPatterns } from '../../../../../plugins/data/public/services'; + +export const SELECT_RANGE_ACTION = 'SELECT_RANGE_ACTION'; + +interface ActionContext { + data: any; + timeFieldName: string; +} + +async function isCompatible(context: ActionContext) { + try { + const filters: esFilters.Filter[] = (await onBrushEvent(context.data, getIndexPatterns)) || []; + return filters.length > 0; + } catch { + return false; + } +} + +export function selectRangeAction( + filterManager: FilterManager, + timeFilter: TimefilterContract +): IAction { + return createAction({ + type: SELECT_RANGE_ACTION, + id: SELECT_RANGE_ACTION, + getDisplayName: () => { + return i18n.translate('data.filter.applyFilterActionTitle', { + defaultMessage: 'Apply filter to current view', + }); + }, + isCompatible, + execute: async ({ timeFieldName, data }: ActionContext) => { + if (!(await isCompatible({ timeFieldName, data }))) { + throw new IncompatibleActionError(); + } + + const filters: esFilters.Filter[] = (await onBrushEvent(data, getIndexPatterns)) || []; + + const selectedFilters: esFilters.Filter[] = mapAndFlattenFilters(filters); + + if (timeFieldName) { + const { timeRangeFilter, restOfFilters } = extractTimeFilter( + timeFieldName, + selectedFilters + ); + filterManager.addFilters(restOfFilters); + if (timeRangeFilter) { + changeTimeFilter(timeFilter, timeRangeFilter); + } + } else { + filterManager.addFilters(selectedFilters); + } + }, + }); +} diff --git a/src/legacy/core_plugins/data/public/actions/value_click_action.ts b/src/legacy/core_plugins/data/public/actions/value_click_action.ts new file mode 100644 index 00000000000000..2f622eb1eb6695 --- /dev/null +++ b/src/legacy/core_plugins/data/public/actions/value_click_action.ts @@ -0,0 +1,126 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { toMountPoint } from '../../../../../plugins/kibana_react/public'; +import { + IAction, + createAction, + IncompatibleActionError, +} from '../../../../../plugins/ui_actions/public'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { getOverlays, getIndexPatterns } from '../../../../../plugins/data/public/services'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { applyFiltersPopover } from '../../../../../plugins/data/public/ui/apply_filters'; +// @ts-ignore +import { createFiltersFromEvent } from './filters/create_filters_from_event'; +import { + esFilters, + FilterManager, + TimefilterContract, + changeTimeFilter, + extractTimeFilter, + mapAndFlattenFilters, +} from '../../../../../plugins/data/public'; + +export const VALUE_CLICK_ACTION = 'VALUE_CLICK_ACTION'; + +interface ActionContext { + data: any; + timeFieldName: string; +} + +async function isCompatible(context: ActionContext) { + try { + const filters: esFilters.Filter[] = (await createFiltersFromEvent(context.data)) || []; + return filters.length > 0; + } catch { + return false; + } +} + +export function valueClickAction( + filterManager: FilterManager, + timeFilter: TimefilterContract +): IAction { + return createAction({ + type: VALUE_CLICK_ACTION, + id: VALUE_CLICK_ACTION, + getDisplayName: () => { + return i18n.translate('data.filter.applyFilterActionTitle', { + defaultMessage: 'Apply filter to current view', + }); + }, + isCompatible, + execute: async ({ timeFieldName, data }: ActionContext) => { + if (!(await isCompatible({ timeFieldName, data }))) { + throw new IncompatibleActionError(); + } + + const filters: esFilters.Filter[] = (await createFiltersFromEvent(data)) || []; + + let selectedFilters: esFilters.Filter[] = mapAndFlattenFilters(filters); + + if (selectedFilters.length > 1) { + const indexPatterns = await Promise.all( + filters.map(filter => { + return getIndexPatterns().get(filter.meta.index!); + }) + ); + + const filterSelectionPromise: Promise = new Promise(resolve => { + const overlay = getOverlays().openModal( + toMountPoint( + applyFiltersPopover( + filters, + indexPatterns, + () => { + overlay.close(); + resolve([]); + }, + (filterSelection: esFilters.Filter[]) => { + overlay.close(); + resolve(filterSelection); + } + ) + ), + { + 'data-test-subj': 'selectFilterOverlay', + } + ); + }); + + selectedFilters = await filterSelectionPromise; + } + + if (timeFieldName) { + const { timeRangeFilter, restOfFilters } = extractTimeFilter( + timeFieldName, + selectedFilters + ); + filterManager.addFilters(restOfFilters); + if (timeRangeFilter) { + changeTimeFilter(timeFilter, timeRangeFilter); + } + } else { + filterManager.addFilters(selectedFilters); + } + }, + }); +} diff --git a/src/legacy/core_plugins/data/public/legacy.ts b/src/legacy/core_plugins/data/public/legacy.ts index a6646ea338c93c..d37c17c2240727 100644 --- a/src/legacy/core_plugins/data/public/legacy.ts +++ b/src/legacy/core_plugins/data/public/legacy.ts @@ -39,8 +39,6 @@ import { plugin } from '.'; const dataPlugin = plugin(); -export const setup = dataPlugin.setup(npSetup.core); +export const setup = dataPlugin.setup(npSetup.core, npSetup.plugins); -export const start = dataPlugin.start(npStart.core, { - data: npStart.plugins.data, -}); +export const start = dataPlugin.start(npStart.core, npStart.plugins); diff --git a/src/legacy/core_plugins/data/public/plugin.ts b/src/legacy/core_plugins/data/public/plugin.ts index 6bd85ef020f167..da35366cdff318 100644 --- a/src/legacy/core_plugins/data/public/plugin.ts +++ b/src/legacy/core_plugins/data/public/plugin.ts @@ -22,6 +22,7 @@ import { DataPublicPluginStart, addSearchStrategy, defaultSearchStrategy, + DataPublicPluginSetup, } from '../../../../plugins/data/public'; import { ExpressionsSetup } from '../../../../plugins/expressions/public'; @@ -32,15 +33,27 @@ import { setInjectedMetadata, setFieldFormats, setSearchService, + setOverlays, // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../../../plugins/data/public/services'; +import { SELECT_RANGE_ACTION, selectRangeAction } from './actions/select_range_action'; +import { VALUE_CLICK_ACTION, valueClickAction } from './actions/value_click_action'; +import { + SELECT_RANGE_TRIGGER, + VALUE_CLICK_TRIGGER, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../plugins/embeddable/public/lib/triggers'; +import { IUiActionsSetup, IUiActionsStart } from '../../../../plugins/ui_actions/public'; export interface DataPluginSetupDependencies { + data: DataPublicPluginSetup; expressions: ExpressionsSetup; + uiActions: IUiActionsSetup; } export interface DataPluginStartDependencies { data: DataPublicPluginStart; + uiActions: IUiActionsStart; } /** @@ -64,19 +77,30 @@ export interface DataStart {} // eslint-disable-line @typescript-eslint/no-empty export class DataPlugin implements Plugin { - public setup(core: CoreSetup) { + public setup(core: CoreSetup, { data, uiActions }: DataPluginSetupDependencies) { setInjectedMetadata(core.injectedMetadata); // This is to be deprecated once we switch to the new search service fully addSearchStrategy(defaultSearchStrategy); + + uiActions.registerAction( + selectRangeAction(data.query.filterManager, data.query.timefilter.timefilter) + ); + uiActions.registerAction( + valueClickAction(data.query.filterManager, data.query.timefilter.timefilter) + ); } - public start(core: CoreStart, { data }: DataPluginStartDependencies): DataStart { + public start(core: CoreStart, { data, uiActions }: DataPluginStartDependencies): DataStart { setUiSettings(core.uiSettings); setQueryService(data.query); setIndexPatterns(data.indexPatterns); setFieldFormats(data.fieldFormats); setSearchService(data.search); + setOverlays(core.overlays); + + uiActions.attachAction(SELECT_RANGE_TRIGGER, SELECT_RANGE_ACTION); + uiActions.attachAction(VALUE_CLICK_TRIGGER, VALUE_CLICK_ACTION); return {}; } diff --git a/src/legacy/core_plugins/data/public/search/expressions/build_tabular_inspector_data.ts b/src/legacy/core_plugins/data/public/search/expressions/build_tabular_inspector_data.ts index 6e6d2a15fa2ac8..8f7953c408a971 100644 --- a/src/legacy/core_plugins/data/public/search/expressions/build_tabular_inspector_data.ts +++ b/src/legacy/core_plugins/data/public/search/expressions/build_tabular_inspector_data.ts @@ -19,9 +19,9 @@ import { set } from 'lodash'; // @ts-ignore -import { createFilter } from '../../../../visualizations/public'; import { FormattedData } from '../../../../../../plugins/inspector/public'; - +// @ts-ignore +import { createFilter } from './create_filter'; interface Column { id: string; name: string; diff --git a/src/legacy/core_plugins/data/public/search/expressions/create_filter.js b/src/legacy/core_plugins/data/public/search/expressions/create_filter.js new file mode 100644 index 00000000000000..3f4028a9b5525b --- /dev/null +++ b/src/legacy/core_plugins/data/public/search/expressions/create_filter.js @@ -0,0 +1,66 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +const getOtherBucketFilterTerms = (table, columnIndex, rowIndex) => { + if (rowIndex === -1) { + return []; + } + + // get only rows where cell value matches current row for all the fields before columnIndex + const rows = table.rows.filter(row => { + return table.columns.every((column, i) => { + return row[column.id] === table.rows[rowIndex][column.id] || i >= columnIndex; + }); + }); + const terms = rows.map(row => row[table.columns[columnIndex].id]); + + return [ + ...new Set( + terms.filter(term => { + const notOther = term !== '__other__'; + const notMissing = term !== '__missing__'; + return notOther && notMissing; + }) + ), + ]; +}; + +const createFilter = (aggConfigs, table, columnIndex, rowIndex, cellValue) => { + const column = table.columns[columnIndex]; + const aggConfig = aggConfigs[columnIndex]; + let filter = []; + const value = rowIndex > -1 ? table.rows[rowIndex][column.id] : cellValue; + if (value === null || value === undefined || !aggConfig.isFilterable()) { + return; + } + if (aggConfig.type.name === 'terms' && aggConfig.params.otherBucket) { + const terms = getOtherBucketFilterTerms(table, columnIndex, rowIndex); + filter = aggConfig.createFilter(value, { terms }); + } else { + filter = aggConfig.createFilter(value); + } + + if (!Array.isArray(filter)) { + filter = [filter]; + } + + return filter; +}; + +export { createFilter }; diff --git a/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts b/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts index 143283152d1044..b4ea2cd378d61b 100644 --- a/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts +++ b/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts @@ -46,6 +46,7 @@ import { Adapters } from '../../../../../../plugins/inspector/public'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { getQueryService, getIndexPatterns } from '../../../../../../plugins/data/public/services'; import { getRequestInspectorStats, getResponseInspectorStats } from '../..'; +import { serializeAggConfig } from './utils'; export interface RequestHandlerParams { searchSource: ISearchSource; @@ -289,6 +290,7 @@ export const esaggs = (): ExpressionFunction { + return { + type: aggConfig.type.name, + indexPatternId: aggConfig.getIndexPattern().id, + aggConfigParams: aggConfig.toJSON().params, + }; +}; + +interface DeserializeAggConfigParams { + type: string; + aggConfigParams: Record; + indexPattern: IndexPattern; +} + +export const deserializeAggConfig = ({ + type, + aggConfigParams, + indexPattern, +}: DeserializeAggConfigParams) => { + const aggConfigs = new AggConfigs(indexPattern); + const aggConfig = aggConfigs.createAggConfig({ + enabled: true, + type, + params: aggConfigParams, + }); + return aggConfig; +}; diff --git a/src/legacy/core_plugins/data/public/search/index.ts b/src/legacy/core_plugins/data/public/search/index.ts index e1c93ec0e3b1c4..c975d5772e0a8b 100644 --- a/src/legacy/core_plugins/data/public/search/index.ts +++ b/src/legacy/core_plugins/data/public/search/index.ts @@ -18,3 +18,4 @@ */ export { getRequestInspectorStats, getResponseInspectorStats } from './utils'; +export { serializeAggConfig } from './expressions/utils'; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/legend.test.tsx b/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/legend.test.tsx index 6f0a5a3784b070..e66dff01b6bf21 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/legend.test.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/legend.test.tsx @@ -34,8 +34,8 @@ jest.mock('@elastic/eui', () => ({ jest.mock('../../../legacy_imports', () => ({ getTableAggs: jest.fn(), })); -jest.mock('../../../../../visualizations/public', () => ({ - createFiltersFromEvent: jest.fn().mockReturnValue(['yes']), +jest.mock('../../../../../data/public/actions/filters/create_filters_from_event', () => ({ + createFiltersFromEvent: jest.fn().mockResolvedValue(['yes']), })); const vis = { @@ -95,8 +95,8 @@ const uiState = { setSilent: jest.fn(), }; -const getWrapper = (props?: Partial) => - mount( +const getWrapper = async (props?: Partial) => { + const wrapper = mount( ) => ); + await (wrapper.find(VisLegend).instance() as VisLegend).refresh(); + wrapper.update(); + return wrapper; +}; + const getLegendItems = (wrapper: ReactWrapper) => wrapper.find('.visLegend__button'); describe('VisLegend Component', () => { @@ -120,9 +125,9 @@ describe('VisLegend Component', () => { }); describe('Legend open', () => { - beforeEach(() => { + beforeEach(async () => { mockState.set('vis.legendOpen', true); - wrapper = getWrapper(); + wrapper = await getWrapper(); }); it('should match the snapshot', () => { @@ -131,9 +136,9 @@ describe('VisLegend Component', () => { }); describe('Legend closed', () => { - beforeEach(() => { + beforeEach(async () => { mockState.set('vis.legendOpen', false); - wrapper = getWrapper(); + wrapper = await getWrapper(); }); it('should match the snapshot', () => { @@ -142,25 +147,26 @@ describe('VisLegend Component', () => { }); describe('Highlighting', () => { - beforeEach(() => { - wrapper = getWrapper(); + beforeEach(async () => { + wrapper = await getWrapper(); }); - it('should call highlight handler when legend item is focused', () => { + it('should call highlight handler when legend item is focused', async () => { const first = getLegendItems(wrapper).first(); + first.simulate('focus'); expect(vislibVis.handler.highlight).toHaveBeenCalledTimes(1); }); - it('should call highlight handler when legend item is hovered', () => { + it('should call highlight handler when legend item is hovered', async () => { const first = getLegendItems(wrapper).first(); first.simulate('mouseEnter'); expect(vislibVis.handler.highlight).toHaveBeenCalledTimes(1); }); - it('should call unHighlight handler when legend item is blurred', () => { + it('should call unHighlight handler when legend item is blurred', async () => { let first = getLegendItems(wrapper).first(); first.simulate('focus'); first = getLegendItems(wrapper).first(); @@ -169,7 +175,7 @@ describe('VisLegend Component', () => { expect(vislibVis.handler.unHighlight).toHaveBeenCalledTimes(1); }); - it('should call unHighlight handler when legend item is unhovered', () => { + it('should call unHighlight handler when legend item is unhovered', async () => { const first = getLegendItems(wrapper).first(); first.simulate('mouseEnter'); @@ -187,8 +193,8 @@ describe('VisLegend Component', () => { }, }; - expect(() => { - wrapper = getWrapper({ vis: newVis }); + expect(async () => { + wrapper = await getWrapper({ vis: newVis }); const first = getLegendItems(wrapper).first(); first.simulate('focus'); first.simulate('blur'); @@ -197,8 +203,8 @@ describe('VisLegend Component', () => { }); describe('Filtering', () => { - beforeEach(() => { - wrapper = getWrapper(); + beforeEach(async () => { + wrapper = await getWrapper(); }); it('should filter out when clicked', () => { @@ -223,8 +229,8 @@ describe('VisLegend Component', () => { }); describe('Toggles details', () => { - beforeEach(() => { - wrapper = getWrapper(); + beforeEach(async () => { + wrapper = await getWrapper(); }); it('should show details when clicked', () => { @@ -236,8 +242,8 @@ describe('VisLegend Component', () => { }); describe('setColor', () => { - beforeEach(() => { - wrapper = getWrapper(); + beforeEach(async () => { + wrapper = await getWrapper(); }); it('sets the color in the UI state', () => { @@ -255,18 +261,18 @@ describe('VisLegend Component', () => { }); describe('toggleLegend function', () => { - it('click should show legend once toggled from hidden', () => { + it('click should show legend once toggled from hidden', async () => { mockState.set('vis.legendOpen', false); - wrapper = getWrapper(); + wrapper = await getWrapper(); const toggleButton = wrapper.find('.visLegend__toggle').first(); toggleButton.simulate('click'); expect(wrapper.exists('.visLegend__list')).toBe(true); }); - it('click should hide legend once toggled from shown', () => { + it('click should hide legend once toggled from shown', async () => { mockState.set('vis.legendOpen', true); - wrapper = getWrapper(); + wrapper = await getWrapper(); const toggleButton = wrapper.find('.visLegend__toggle').first(); toggleButton.simulate('click'); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/legend.tsx b/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/legend.tsx index 0eec557dd334ee..a170af33583df0 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/legend.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/legend.tsx @@ -24,7 +24,8 @@ import { i18n } from '@kbn/i18n'; import { EuiPopoverProps, EuiIcon, keyCodes, htmlIdGenerator } from '@elastic/eui'; // @ts-ignore -import { createFiltersFromEvent } from '../../../../../visualizations/public'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { createFiltersFromEvent } from '../../../../../data/public/actions/filters/create_filters_from_event'; import { CUSTOM_LEGEND_VIS_TYPES, LegendItem } from './models'; import { VisLegendItem } from './legend_item'; import { getPieNames } from './pie_utils'; @@ -94,11 +95,11 @@ export class VisLegend extends PureComponent { this.props.vis.API.events.filter({ data, negate }); }; - canFilter = (item: LegendItem): boolean => { + canFilter = async (item: LegendItem): Promise => { if (CUSTOM_LEGEND_VIS_TYPES.includes(this.props.vislibVis.visConfigArgs.type)) { return false; } - const filters = createFiltersFromEvent({ aggConfigs: this.state.tableAggs, data: item.values }); + const filters = await createFiltersFromEvent({ data: item.values }); return Boolean(filters.length); }; @@ -123,16 +124,39 @@ export class VisLegend extends PureComponent { }; // Most of these functions were moved directly from the old Legend class. Not a fan of this. - getLabels = (data: any, type: string) => { - if (!data) return []; - data = data.columns || data.rows || [data]; + setLabels = (data: any, type: string): Promise => + new Promise(async resolve => { + let labels = []; + if (CUSTOM_LEGEND_VIS_TYPES.includes(type)) { + const legendLabels = this.props.vislibVis.getLegendLabels(); + if (legendLabels) { + labels = map(legendLabels, label => { + return { label }; + }); + } + } else { + if (!data) return []; + data = data.columns || data.rows || [data]; - if (type === 'pie') return getPieNames(data); + labels = type === 'pie' ? getPieNames(data) : this.getSeriesLabels(data); + } - return this.getSeriesLabels(data); - }; + const labelsConfig = await Promise.all( + labels.map(async label => ({ + ...label, + canFilter: await this.canFilter(label), + })) + ); + + this.setState( + { + labels: labelsConfig, + }, + resolve + ); + }); - refresh = () => { + refresh = async () => { const vislibVis = this.props.vislibVis; if (!vislibVis || !vislibVis.visConfig) { this.setState({ @@ -154,24 +178,12 @@ export class VisLegend extends PureComponent { this.setState({ open: this.props.vis.params.addLegend }); } - if (CUSTOM_LEGEND_VIS_TYPES.includes(vislibVis.visConfigArgs.type)) { - const legendLabels = this.props.vislibVis.getLegendLabels(); - if (legendLabels) { - this.setState({ - labels: map(legendLabels, label => { - return { label }; - }), - }); - } - } else { - this.setState({ labels: this.getLabels(this.props.visData, vislibVis.visConfigArgs.type) }); - } - if (vislibVis.visConfig) { this.getColor = this.props.vislibVis.visConfig.data.getColorFunc(); } this.setState({ tableAggs: getTableAggs(this.props.vis) }); + await this.setLabels(this.props.visData, vislibVis.visConfigArgs.type); }; highlight = (event: BaseSyntheticEvent) => { @@ -219,7 +231,7 @@ export class VisLegend extends PureComponent { key={item.label} anchorPosition={anchorPosition} selected={this.state.selectedLabel === item.label} - canFilter={this.canFilter(item)} + canFilter={item.canFilter} onFilter={this.filter} onSelect={this.toggleDetails} legendId={this.legendId} diff --git a/src/legacy/core_plugins/visualizations/public/embeddable/visualize_embeddable.ts b/src/legacy/core_plugins/visualizations/public/embeddable/visualize_embeddable.ts index 2af468ff77de6e..d3badcc6bdc3fc 100644 --- a/src/legacy/core_plugins/visualizations/public/embeddable/visualize_embeddable.ts +++ b/src/legacy/core_plugins/visualizations/public/embeddable/visualize_embeddable.ts @@ -17,13 +17,12 @@ * under the License. */ -import _, { forEach } from 'lodash'; +import _ from 'lodash'; import { PersistedState } from 'ui/persisted_state'; import { Subscription } from 'rxjs'; import * as Rx from 'rxjs'; import { buildPipeline } from 'ui/visualize/loader/pipeline_helpers'; import { SavedObject } from 'ui/saved_objects/types'; -import { getTableAggs } from 'ui/visualize/loader/pipeline_helpers/utilities'; import { AppState } from 'ui/state_management/app_state'; import { npStart } from 'ui/new_platform'; import { IExpressionLoaderParams } from 'src/plugins/expressions/public'; @@ -34,7 +33,6 @@ import { Query, onlyDisabledFiltersChanged, esFilters, - mapAndFlattenFilters, ISearchSource, } from '../../../../../plugins/data/public'; import { @@ -42,7 +40,8 @@ import { EmbeddableOutput, Embeddable, Container, - APPLY_FILTER_TRIGGER, + VALUE_CLICK_TRIGGER, + SELECT_RANGE_TRIGGER, } from '../../../../../plugins/embeddable/public'; import { dispatchRenderComplete } from '../../../../../plugins/kibana_utils/public'; import { SavedSearch } from '../../../kibana/public/discover/np_ready/types'; @@ -105,7 +104,6 @@ export class VisualizeEmbeddable extends Embeddable { - if (event.disabled || !eventName) { - return; - } else { - this.actions[eventName] = event.defaultAction; - } - }); - // This is a hack to give maps visualizations access to data in the // globalState, since they can no longer access it via searchSource. // TODO: Remove this as a part of elastic/kibana#30593 @@ -301,18 +290,13 @@ export class VisualizeEmbeddable extends Embeddable { - if (this.actions[event.name]) { - event.data.aggConfigs = getTableAggs(this.vis); - const filters: esFilters.Filter[] = this.actions[event.name](event.data) || []; - const mappedFilters = mapAndFlattenFilters(filters); - const timeFieldName = this.vis.indexPattern.timeFieldName; - - npStart.plugins.uiActions.executeTriggerActions(APPLY_FILTER_TRIGGER, { - embeddable: this, - filters: mappedFilters, - timeFieldName, - }); - } + const eventName = event.name === 'brush' ? SELECT_RANGE_TRIGGER : VALUE_CLICK_TRIGGER; + + npStart.plugins.uiActions.executeTriggerActions(eventName, { + embeddable: this, + timeFieldName: this.vis.indexPattern.timeFieldName, + data: event.data, + }); }) ); diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/filters/index.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/filters/index.ts deleted file mode 100644 index 4558621dc6615a..00000000000000 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/filters/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -// @ts-ignore -export * from './vis_filters'; diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/index.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/index.ts index 4dffcb8ce995e3..3c4a1c1449d475 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/index.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/index.ts @@ -44,7 +44,6 @@ export function plugin(initializerContext: PluginInitializerContext) { /** @public static code */ export { Vis, VisParams, VisState } from './vis'; -export * from './filters'; export { TypesService } from './types/types_service'; export { Status } from './legacy/update_status'; @@ -53,6 +52,4 @@ export { buildPipeline, buildVislibDimensions, SchemaConfig } from './legacy/bui // @ts-ignore export { updateOldState } from './legacy/vis_update_state'; export { calculateObjectHash } from './legacy/calculate_object_hash'; -// @ts-ignore -export { createFiltersFromEvent } from './filters/vis_filters'; export { createSavedVisLoader } from '../../saved_visualizations/saved_visualizations'; diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/types/base_vis_type.js b/src/legacy/core_plugins/visualizations/public/np_ready/public/types/base_vis_type.js index f62b3a0b393aca..351acc48e26766 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/types/base_vis_type.js +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/types/base_vis_type.js @@ -19,7 +19,6 @@ import _ from 'lodash'; -import { createFiltersFromEvent, onBrushEvent } from '../filters'; import { DefaultEditorController } from '../../../../../vis_default_editor/public'; export class BaseVisType { @@ -60,15 +59,6 @@ export class BaseVisType { showIndexSelection: true, hierarchicalData: false, // we should get rid of this i guess ? }, - events: { - filterBucket: { - defaultAction: createFiltersFromEvent, - }, - brush: { - defaultAction: onBrushEvent, - disabled: true, - }, - }, stage: 'production', feedbackMessage: '', hidden: false, diff --git a/src/legacy/ui/public/agg_types/agg_config.ts b/src/legacy/ui/public/agg_types/agg_config.ts index 3f88c540be1641..17a8b14b57d020 100644 --- a/src/legacy/ui/public/agg_types/agg_config.ts +++ b/src/legacy/ui/public/agg_types/agg_config.ts @@ -63,7 +63,7 @@ const unknownSchema: Schema = { const getTypeFromRegistry = (type: string): AggType => { // We need to inline require here, since we're having a cyclic dependency // from somewhere inside agg_types back to AggConfig. - const aggTypes = require('../agg_types').aggTypes; + const aggTypes = require('./agg_types').aggTypes; const registeredType = aggTypes.metrics.find((agg: AggType) => agg.name === type) || aggTypes.buckets.find((agg: AggType) => agg.name === type); diff --git a/src/legacy/ui/public/agg_types/agg_types.ts b/src/legacy/ui/public/agg_types/agg_types.ts new file mode 100644 index 00000000000000..1b05f5926ebfc9 --- /dev/null +++ b/src/legacy/ui/public/agg_types/agg_types.ts @@ -0,0 +1,92 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { countMetricAgg } from './metrics/count'; +import { avgMetricAgg } from './metrics/avg'; +import { sumMetricAgg } from './metrics/sum'; +import { medianMetricAgg } from './metrics/median'; +import { minMetricAgg } from './metrics/min'; +import { maxMetricAgg } from './metrics/max'; +import { topHitMetricAgg } from './metrics/top_hit'; +import { stdDeviationMetricAgg } from './metrics/std_deviation'; +import { cardinalityMetricAgg } from './metrics/cardinality'; +import { percentilesMetricAgg } from './metrics/percentiles'; +import { geoBoundsMetricAgg } from './metrics/geo_bounds'; +import { geoCentroidMetricAgg } from './metrics/geo_centroid'; +import { percentileRanksMetricAgg } from './metrics/percentile_ranks'; +import { derivativeMetricAgg } from './metrics/derivative'; +import { cumulativeSumMetricAgg } from './metrics/cumulative_sum'; +import { movingAvgMetricAgg } from './metrics/moving_avg'; +import { serialDiffMetricAgg } from './metrics/serial_diff'; +import { dateHistogramBucketAgg } from './buckets/date_histogram'; +import { histogramBucketAgg } from './buckets/histogram'; +import { rangeBucketAgg } from './buckets/range'; +import { dateRangeBucketAgg } from './buckets/date_range'; +import { ipRangeBucketAgg } from './buckets/ip_range'; +import { termsBucketAgg } from './buckets/terms'; +import { filterBucketAgg } from './buckets/filter'; +import { filtersBucketAgg } from './buckets/filters'; +import { significantTermsBucketAgg } from './buckets/significant_terms'; +import { geoHashBucketAgg } from './buckets/geo_hash'; +import { geoTileBucketAgg } from './buckets/geo_tile'; +import { bucketSumMetricAgg } from './metrics/bucket_sum'; +import { bucketAvgMetricAgg } from './metrics/bucket_avg'; +import { bucketMinMetricAgg } from './metrics/bucket_min'; +import { bucketMaxMetricAgg } from './metrics/bucket_max'; + +export { AggType } from './agg_type'; + +export const aggTypes = { + metrics: [ + countMetricAgg, + avgMetricAgg, + sumMetricAgg, + medianMetricAgg, + minMetricAgg, + maxMetricAgg, + stdDeviationMetricAgg, + cardinalityMetricAgg, + percentilesMetricAgg, + percentileRanksMetricAgg, + topHitMetricAgg, + derivativeMetricAgg, + cumulativeSumMetricAgg, + movingAvgMetricAgg, + serialDiffMetricAgg, + bucketAvgMetricAgg, + bucketSumMetricAgg, + bucketMinMetricAgg, + bucketMaxMetricAgg, + geoBoundsMetricAgg, + geoCentroidMetricAgg, + ], + buckets: [ + dateHistogramBucketAgg, + histogramBucketAgg, + rangeBucketAgg, + dateRangeBucketAgg, + ipRangeBucketAgg, + termsBucketAgg, + filterBucketAgg, + filtersBucketAgg, + significantTermsBucketAgg, + geoHashBucketAgg, + geoTileBucketAgg, + ], +}; diff --git a/src/legacy/ui/public/agg_types/index.ts b/src/legacy/ui/public/agg_types/index.ts index ca7c2f82023c94..cf2733b9a9f36a 100644 --- a/src/legacy/ui/public/agg_types/index.ts +++ b/src/legacy/ui/public/agg_types/index.ts @@ -17,80 +17,7 @@ * under the License. */ -import { countMetricAgg } from './metrics/count'; -import { avgMetricAgg } from './metrics/avg'; -import { sumMetricAgg } from './metrics/sum'; -import { medianMetricAgg } from './metrics/median'; -import { minMetricAgg } from './metrics/min'; -import { maxMetricAgg } from './metrics/max'; -import { topHitMetricAgg } from './metrics/top_hit'; -import { stdDeviationMetricAgg } from './metrics/std_deviation'; -import { cardinalityMetricAgg } from './metrics/cardinality'; -import { percentilesMetricAgg } from './metrics/percentiles'; -import { geoBoundsMetricAgg } from './metrics/geo_bounds'; -import { geoCentroidMetricAgg } from './metrics/geo_centroid'; -import { percentileRanksMetricAgg } from './metrics/percentile_ranks'; -import { derivativeMetricAgg } from './metrics/derivative'; -import { cumulativeSumMetricAgg } from './metrics/cumulative_sum'; -import { movingAvgMetricAgg } from './metrics/moving_avg'; -import { serialDiffMetricAgg } from './metrics/serial_diff'; -import { dateHistogramBucketAgg, setBounds } from './buckets/date_histogram'; -import { histogramBucketAgg } from './buckets/histogram'; -import { rangeBucketAgg } from './buckets/range'; -import { dateRangeBucketAgg } from './buckets/date_range'; -import { ipRangeBucketAgg } from './buckets/ip_range'; -import { termsBucketAgg, termsAggFilter } from './buckets/terms'; -import { filterBucketAgg } from './buckets/filter'; -import { filtersBucketAgg } from './buckets/filters'; -import { significantTermsBucketAgg } from './buckets/significant_terms'; -import { geoHashBucketAgg } from './buckets/geo_hash'; -import { geoTileBucketAgg } from './buckets/geo_tile'; -import { bucketSumMetricAgg } from './metrics/bucket_sum'; -import { bucketAvgMetricAgg } from './metrics/bucket_avg'; -import { bucketMinMetricAgg } from './metrics/bucket_min'; -import { bucketMaxMetricAgg } from './metrics/bucket_max'; - -export { AggType } from './agg_type'; - -export const aggTypes = { - metrics: [ - countMetricAgg, - avgMetricAgg, - sumMetricAgg, - medianMetricAgg, - minMetricAgg, - maxMetricAgg, - stdDeviationMetricAgg, - cardinalityMetricAgg, - percentilesMetricAgg, - percentileRanksMetricAgg, - topHitMetricAgg, - derivativeMetricAgg, - cumulativeSumMetricAgg, - movingAvgMetricAgg, - serialDiffMetricAgg, - bucketAvgMetricAgg, - bucketSumMetricAgg, - bucketMinMetricAgg, - bucketMaxMetricAgg, - geoBoundsMetricAgg, - geoCentroidMetricAgg, - ], - buckets: [ - dateHistogramBucketAgg, - histogramBucketAgg, - rangeBucketAgg, - dateRangeBucketAgg, - ipRangeBucketAgg, - termsBucketAgg, - filterBucketAgg, - filtersBucketAgg, - significantTermsBucketAgg, - geoHashBucketAgg, - geoTileBucketAgg, - ], -}; - +export { aggTypes } from './agg_types'; export { AggParam } from './agg_params'; export { AggConfig } from './agg_config'; export { AggConfigs } from './agg_configs'; @@ -99,5 +26,6 @@ export { FieldParamType } from './param_types'; export { BUCKET_TYPES } from './buckets/bucket_agg_types'; export { METRIC_TYPES } from './metrics/metric_agg_types'; export { ISchemas, Schema, Schemas } from './schemas'; - -export { setBounds, termsAggFilter }; +export { AggType } from './agg_type'; +export { setBounds } from './buckets/date_histogram'; +export { termsAggFilter } from './buckets/terms'; diff --git a/src/legacy/ui/public/agg_types/param_types/field.ts b/src/legacy/ui/public/agg_types/param_types/field.ts index 4ce5bb29f8ff60..d01e059c6c616b 100644 --- a/src/legacy/ui/public/agg_types/param_types/field.ts +++ b/src/legacy/ui/public/agg_types/param_types/field.ts @@ -22,7 +22,7 @@ import { i18n } from '@kbn/i18n'; import { AggConfig } from '../agg_config'; import { SavedObjectNotFound } from '../../../../../plugins/kibana_utils/public'; import { BaseParamType } from './base'; -import { toastNotifications } from '../../notify'; +import { npStart } from '../../new_platform'; import { propFilter } from '../filter'; import { Field, IFieldList } from '../../../../../plugins/data/public'; import { isNestedField } from '../../../../../plugins/data/public'; @@ -89,7 +89,7 @@ export class FieldParamType extends BaseParamType { (f: any) => f.name === fieldName ); if (!validField) { - toastNotifications.addDanger( + npStart.core.notifications.toasts.addDanger( i18n.translate( 'common.ui.aggTypes.paramTypes.field.invalidSavedFieldParameterErrorMessage', { diff --git a/src/legacy/ui/public/agg_types/utils.ts b/src/legacy/ui/public/agg_types/utils.ts index fd405d49625eda..e382f821b31a90 100644 --- a/src/legacy/ui/public/agg_types/utils.ts +++ b/src/legacy/ui/public/agg_types/utils.ts @@ -17,7 +17,7 @@ * under the License. */ -import { isValidEsInterval } from '../../../core_plugins/data/public'; +import { isValidEsInterval } from '../../../core_plugins/data/common/parse_es_interval/is_valid_es_interval'; import { leastCommonInterval } from '../vis/lib/least_common_interval'; /** diff --git a/src/legacy/ui/public/vis/lib/least_common_interval.ts b/src/legacy/ui/public/vis/lib/least_common_interval.ts index 244bc1d0111e3b..72426855f70af5 100644 --- a/src/legacy/ui/public/vis/lib/least_common_interval.ts +++ b/src/legacy/ui/public/vis/lib/least_common_interval.ts @@ -19,7 +19,7 @@ import dateMath from '@elastic/datemath'; import { leastCommonMultiple } from './least_common_multiple'; -import { parseEsInterval } from '../../../../core_plugins/data/public'; +import { parseEsInterval } from '../../../../core_plugins/data/common/parse_es_interval/parse_es_interval'; /** * Finds the lowest common interval between two given ES date histogram intervals diff --git a/src/plugins/embeddable/public/bootstrap.ts b/src/plugins/embeddable/public/bootstrap.ts index 801329a4a79af9..1e0e7dfdb09331 100644 --- a/src/plugins/embeddable/public/bootstrap.ts +++ b/src/plugins/embeddable/public/bootstrap.ts @@ -23,6 +23,8 @@ import { APPLY_FILTER_TRIGGER, createFilterAction, PANEL_BADGE_TRIGGER, + SELECT_RANGE_TRIGGER, + VALUE_CLICK_TRIGGER, } from './lib'; /** @@ -50,11 +52,25 @@ export const bootstrap = (uiActions: IUiActionsSetup) => { description: 'Actions appear in title bar when an embeddable loads in a panel', actionIds: [], }; + const selectRangeTrigger = { + id: SELECT_RANGE_TRIGGER, + title: 'Select range', + description: 'Applies a range filter', + actionIds: [], + }; + const valueClickTrigger = { + id: VALUE_CLICK_TRIGGER, + title: 'Value clicked', + description: 'Value was clicked', + actionIds: [], + }; const actionApplyFilter = createFilterAction(); uiActions.registerTrigger(triggerContext); uiActions.registerTrigger(triggerFilter); uiActions.registerAction(actionApplyFilter); uiActions.registerTrigger(triggerBadge); + uiActions.registerTrigger(selectRangeTrigger); + uiActions.registerTrigger(valueClickTrigger); // uiActions.attachAction(triggerFilter.id, actionApplyFilter.id); }; diff --git a/src/plugins/embeddable/public/index.ts b/src/plugins/embeddable/public/index.ts index 583b21ddfa4338..ec71a1e724c7d8 100644 --- a/src/plugins/embeddable/public/index.ts +++ b/src/plugins/embeddable/public/index.ts @@ -25,6 +25,8 @@ export { APPLY_FILTER_ACTION, APPLY_FILTER_TRIGGER, PANEL_BADGE_TRIGGER, + SELECT_RANGE_TRIGGER, + VALUE_CLICK_TRIGGER, Adapters, AddPanelAction, CONTEXT_MENU_TRIGGER, diff --git a/src/plugins/embeddable/public/lib/triggers/index.ts b/src/plugins/embeddable/public/lib/triggers/index.ts index ffa7f6d0c0f448..72565b3f527adc 100644 --- a/src/plugins/embeddable/public/lib/triggers/index.ts +++ b/src/plugins/embeddable/public/lib/triggers/index.ts @@ -19,4 +19,6 @@ export const CONTEXT_MENU_TRIGGER = 'CONTEXT_MENU_TRIGGER'; export const APPLY_FILTER_TRIGGER = 'FILTER_TRIGGER'; +export const SELECT_RANGE_TRIGGER = 'SELECT_RANGE_TRIGGER'; +export const VALUE_CLICK_TRIGGER = 'VALUE_CLICK_TRIGGER'; export const PANEL_BADGE_TRIGGER = 'PANEL_BADGE_TRIGGER'; diff --git a/src/plugins/expressions/common/expression_types/kibana_datatable.ts b/src/plugins/expressions/common/expression_types/kibana_datatable.ts index c360a2be8c7f7b..38227d2ed6207b 100644 --- a/src/plugins/expressions/common/expression_types/kibana_datatable.ts +++ b/src/plugins/expressions/common/expression_types/kibana_datatable.ts @@ -23,9 +23,16 @@ import { Datatable, PointSeries } from '.'; const name = 'kibana_datatable'; +export interface KibanaDatatableColumnMeta { + type: string; + indexPatternId?: string; + aggConfigParams?: Record; +} + export interface KibanaDatatableColumn { id: string; name: string; + meta?: KibanaDatatableColumnMeta; formatHint?: SerializedFieldFormat; } diff --git a/test/interpreter_functional/screenshots/baseline/combined_test.png b/test/interpreter_functional/screenshots/baseline/combined_test.png index 87f3173d5602472d092da015c8e58f354f0f9bb3..56c055b8b1cff85d7dd891750efdcf6aa198e937 100644 GIT binary patch literal 16994 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 literal 22801 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 W-FKg^bGCg43Sv)JKbLh*2~7aN3?W$n diff --git a/test/interpreter_functional/screenshots/baseline/final_screenshot_test.png b/test/interpreter_functional/screenshots/baseline/final_screenshot_test.png index d860bb73521ceaf40d4b03e271b068d5fb18fbb7..753ab2c2c6e9492950600b52b61a8241c79ddc53 100644 GIT binary patch literal 17033 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 literal 22852 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 Om~LLP0HLmWB>fNO{2NjL literal 32900 zcmeFZXINEPvo6{yDu_rD$sjrB90Wx&i;SQGB3X%&Gc*|_Ne~typdcV1f+!h5at28f zB?rk#V3BXl{<_a|_c{A~``r8E-anhiZkKDOF>2JPdf&Hd60V`9fOm!B${&CHfv2P> ztM$hpSaE;+agG!F5`2RuN&foBAAhPS$==a%Ket+k>)q`#+j2HkGiFm$I?}8wus~Jz zy65M0In7G@=R{%&@80W(r{8;rO}BKDI)SZ{L+SYoY_aqo=ny{B*-GKn6^hc6jiarz zGnzA+{n_Xhr?pYn-lK!7?585K|NL|}$o%uM5%&iDKR=I(2nhfF*$M~_`{#$0^PiJt zs~^+-bMnkZ+<#6cYgx4)^jkvXnXRUYYRLmV*KIl`B`CRX90RjF%hQMOTbd-CCGQaC-j! zy<(nDc$V_Z`+1Gc1jNLuD3oAbU0s;s)RjB3+{&TX%s<&Q^>9A?ec`EBA73mQ{Z0DJ z;ntAr5-N&f5a)V#uJ#B0GXHrF-L0Q9OlUW%73qI1cNCX@@AN)1^);*|G%|EEBt0BIR+l}U z?QKb)#ca)D1t`B{-#T`RAtt6)eagj;Iu1`ra4cf9ME;>eW=$zP{y^7#vY!xiv#OQvMXT#*r$tkd$*~ zF7zTOICz=k83nJ4flvgM#L~GYTcT;XT{OM zxTM-;cD7anitj#%MmI#dS!~t+a#$Tp@~?fp?LJ&&EP#81lU}@%e|Pyub1`Ro+^lG) zyX6g@NSc-!A973wTTCb(rSRv+b@UmT9NL9;wwxU@j4!A~(QWS)(%-(TbX$?p(sEIb z|F18j*Fbh#sKTva36Yq%CleoVY{yDv<}h2t-p6eQ=aph8D`W3-o%3U7XE&)6ncSLc zQ2*r7A>931EU@-n_Byl2Iw@k7S|OX;!Bw6=>!*1i^V3K@G?3O86=2$rEgPt(pLZGfH~LI&lyF^dXk7ZSO>~o!V#Leu3HFKR(fdhyq)O zNl-AM?~6$j`#S9b*^2K^#iwHPTbupugU4w#Nosm7H$_F!#YO-2I9cp%9Rq_-fu1Nj zfgJ*-!LRzHadCOt4|PA>Z#rS1HT_vg$;Oo{ZdeH!8dq9zIAAyG<;4I7twT+h?C!MeaD{ zV>3}4RlGfBUv>ZYPomLMJEfQGpAtgmDvUP!&|6>^R@|dK^CF)kHF3AfxGN+wY^rTe zwNqJqvtSgB8+&UVkB|0Jo!6b2NZ5nuiJsi>t4&asI-&CWHBo@8q%_Ee_rE`#NO5mN z`kSe`RE)Hs{g0qhTYb7>H=|3FoCc`>F<*TBQoF@yI)>Kn?zfzC^VEw+8%d7yFZBqh z4U_v`RB}X}3*0caY`0H%dF&l3H_26Z^>4p)>-<$E};}r=dkTsAM z6%$j~Duem)E=4URl_mODA&T$ht;iuAJOr$1eAt*W?hJ5rfiwc+uic)@Xz zAJa{Eyf?;u%yr9$cK@m$>~U87jp}{6owJfZ2-#?FfFC+gg=)q~MRlaAc)(-GcYg^$ zF2kt&^CT5zW%j$FQFE=aHpg7x@+ePJ@p&dDrk8IU>BP*PGd|5X%AB^IZ86XV*uZ!`YoBB7sCg;NFDKZDzz5xo|;PSqU}rN5bU;j#d^ z?<6Z#%Uj8>!l!tNTt?wx3AbtjsY?pIn)DqD4ol!d2G^N#Mj~DK@a~)YhqLOO$F6)#u}N;P`3g~t8j$ce+j7#U z@jX0R$amUh&&#>GUQ72rB&VNW8eqfciAlYEv!g?z8?DDo#9I8>2{YHS%X;f~wl zK){CAVqnM3+|&fC>O$miR^OO=os?9Y2)Q02DOOzBJrxxL@MQCC&LDMDeLZg@FO;hEG`#xG1rru>DAPAFbY0{_D&Z z^hui~CeU__k?CFwt?z>!jw;t2I(l;!NRf|^tF7>mkY8VFyH4%O!u3#)i7i*$wdwhB z=&UFeMCw;UE6Gx(%r&j8qOFB1B7HT6)~Eub+Hm_CLzI&9>i0Axqm)?T47&-uvmUSEZpF-e4Zn<1rF3&jGpRkAiXWi_?(h z^V#|*rb`?$3E8Y+bX&cvX6Hu^cT+G6V@(ONE!WnTTz?Ab^WnKW};RqZO~nXLv< z^vv1(u-I)dBhs^nw_y<8yN8WWomZb-B;g1#wc2Az*Ce`dIH|n9Zm9S{H!3Vl`B*_y zlTabr?6c{jJu00q9&*3;*0e%fB;}DLy_T>H#E$0ip3`8SO4p^%_n{jMAFW7EQ*6fs z%Rflrap$YY(EM~?DM;ot_6Kku z8!NH6eWrgOv$h{eEkbC-sb3SfyCTw?t*U||xp9MkZ!jE_r3fP}HI&yzJD9H@ zy0`XF;LRBq=kcCbb971=5A4h~FyNE^Y$kg1tfHb9e5OV$FaFR&G(X)RC6j?ksck^w$_t1_T6PG84I8_2ygR3)_v>_xDr> z!6LCGNhFP=`!*N74djb;`c2m6v^X14Q#R2!b>}2J|6%Z%#e*9-I7x9Yp0TBVT>b`z zZ2GdaVPdru!$G$D9eZXbkDflOM@E(#8?qUKX|mU8#L*6>=`3L8ZsdQD+)r^ru91i3 z3Rw?1@$L{X|M+02Zeekw21zR$)k4XmC05&(GBPs120vf^Y*7<#JBFFrRq7Y@+T+ar ze{e3n`TUND5#r^yOD)zs0Z?wr?b{i;FBY`@wS2@xL^6eGx6nd6sy1WIq=o9QngKDh z;j*z+VFW#B=(BRm%h8Zns$GCeVC9XEOWyF_p{_x)t+%=}!lX#nQWd zV*sVjMC@Dm%EDBbUDq*qMpr4t(FzK;ZW|1;zg7n*LngQ>OylRwSTkJ4bAgbJ5(3Sg z;ffs*)L^b*yu{_n=-jt%co`4+o6)0XZqm#(56a1mzC4)->g|zMa(5@2c+yu+*q>b; z!qd0EzPNbLns>LWz6p~dx5EtHrBvV0uqhwk9EBF17xIp+ag&A^8mRa{)R{Fx$sWb> z-RJW&zM3b!)i{~yesPhK&K6m3 z?|bJ5t&XMp=lcEr)&0Hdl5w8; zMw-FM))kbwg*k*RP`S9VL-A?r27WE3GYSe~X#)p}^jLa;qHY%tC<9E^kyzN6U^^D+ zb08KF7|7TArEOmF*(9&`r^6%{H1I#zPej_KwgP+ei`dt$H3QsE+WF?wu-UY1^n*_8 z#-kM8i*Q&w@xQoLLTN?%NyL|1OXwn7Nr3uqHL8%Xh`1z(q!=V45~t(C@ymm+#&!qaPMKUj)PvmXHls&n5}gn#`nCZM@MfU~KQhS4o_X2&JV^ zGcdU4%{R5DucVw8oy(-6r=g>RdMfseHy%QAvuu81S4Ebax-6>VX9YSe|KZEq3E=(6 zte0VHL8xjb?Rq7g-P3i}r~`@9Yk#dp|LN0O(+s6qd7V77+5Cq>@w_fmkC`D%a>~vH zyhu(C4nN#o*3gPN{qYC!OeZkK!PSf0|`BriYvrcxgiZ!4cRV{X8ms;n2=- z>!taFs}-hyWK*>myN%K)-*s@{D~w7H$jwP}N8jYYCe*fb<7Ng%%|ze>bhm~~a7tJ? z!|zOgv#HXBa#I&r8|sVo(5pCX9GthlU&WDtEi2nDj}2Q40~n2YdEld*(qMr$E-_?= z*(lh|u!&j*{v10Zx^Y8Z$-rxE5S6+o%neWZ@p0^S!=+uZ^#6mC0N?ay*G9~WqTTnF zTHb5CJ#Wq?A`k*&O)Lu8!yXi|6`|8T1@!8{2$L+-uQ-L#sExW zJnQID;{z-54+ZUI9;m1sQ5!i|EU+u*$vk2GS>6LQ zuK=9)IKy@ARhZ%kHpnH|`%&d7#ffP(_{0tl_coh9{`BH~iALWBD1*6MRa%f7FG)+R zU$%CWXG0YBumg9M>qec9iOG3DFH`tyqoo$e_-uZb23A+k>U^XM4yy$$9_e9zp4nz> z;q+)dTXJ>u>t*fma2y1;NPn>&dnMMk!}rN$?ArErw04E#_0#R<1VH0~;iPF^0axC> z4a2n0JDSq7^T+M5v9jJ25y9sWMYDh6yPqicbacl{yynTzN~Y8A8FJd*pPu&Xin~?^ z-2QB;a3$MnQ2AerMej%NGGvEpY;K;3N@dGMFFW76e4OR+xfx@!lW6O_J`n3Pk{tIu z7g*^xBjAjYJb)AB7uBc1Wmc<2<#sSB>`y0;FZ-#GFhb0ybFqH@O^YxLKK0l>}lwlgqGr1d~~?V%6!%JVD$sye~YxwOgP>eFFZN_Sd zwYyS;>Wpm0vH?}Sd-u+So@J5s^cl#ENiAyMWI?B4SpSL}8`s_@7E+eSWYB&l=6j6w zSqc-xQ+s-&#X&UbFD>XZCg$Mm;kTf0`@W+Ezh~Nol)0L>V&e|ETtu=*OP>Zym)ML% z7L2(*rsr7U^`8T=QF3eGiNU%7>UYgSx~#nXO?q<%EW+3`<3oJ`zQIykj_yTFI5Fb5 zd7dUh$GK_+ul^bxL<1)=-x(|b?&!R<`*?nN+WE|XO|h%%ZX@mU=g-OZml;vFPJb!3 zcWe=}02!{+*=@AKXs13D0>;-QYiCDux_SCx1iAN2cnd8(KmYmIIC0NavWZGYskAf9 z@``=3r1_=j*(sno2!ZH4ofG)DJ|3HsMkR-G7OYWL{TY=DIR>Mx6$Qz`paSc8S4tV5 z?O_!nwc(7Fk~?e|!d}CP>`k*zV3A@OK z=we(_N+=Hz5qDF1-jHIWj@^WV?D4dbk#EBuH5(dWpbsOXfwIT{dLdJZwHpOcspiI) z)P+;8@(X1eX%SqeJDB&Wn$P@FI&)8h5h6`?Cm5n$^M-`rEzd>(1Vb?~>o%kl<=<;a zkPu1Q*1CVcHI`w-jH=a4ZCer`VGK{2=L{W)aD=hN!~tQgv{X{ByR-q&=2{nief8$e z&F)3Bnn1R(((thNLDKisKZLL)b#zs{?sVfd9}^}de7Tvk96rc9?^r>|mdl_}>|tAf z9VKX^k*;sQB&9~f=T7QZ~!)YPOK5mRXE?96xv zTm)6NQVCwgfp}F6$h(;|J`^nuyo2DMGf~Ihm$gf*n5OMPb4EK>If*6x&)>kvafow_ zmy>g7nLx$bPDtN)oCoOPm`h6P8j?Pl`0P*h-mNP)G@8al&c&(B5N?G+iaXzi%M;x& z?+T2hz{safsmI>2kP{Jr!U{;l!>69?m7rZv zSOPvS;XwzCbYXDIzR2c4+1p_#^}?pEZ%tq>4Ch~?42_w4#(i*bEgQkoe~pq_DlRar zFgjhd&U$w88z5q;yBid;H(Pt$cu8P@wZWmG#>)^oy@gJ9bJ3oAxe9<%4%Gt+W;`_IthvKgB#4g_lqO%&&7 zNE8N&iTnJZ5<-+0S1H^nnYVX#_(*`!`{Cl@HIYU;Jx6Fc&%wa~`U-CTpS8guAv3rw zVscQ@kwt+>naXoo@(1zHDwpXEy)R4}bBl|n?3^FYpWdLLc+Nw$cpcfGx{oFtfGh`v zV=$uY6#UqPei93g+FGTyhzHEf<`J1){N%Myqve*Rsiop)P}lXs(>Dz0D(gwm~6W`HvM*zj*d=e!cze>UHQ0quC|(!)i%LNTwsce%xgR* zj2}g%b`u95zeQ(yMCQ2Q{d;U9D0e(I8U5|%0QT&;wL>;NZ5)P48Cz@CgSo$L!Tb4S zq?nvU=I%k}XUm>Akl=V~jzfADeZmWGa2`hs+dg#UHa0d!i+e3x&3ao@M4mJPseY|=0hVG(NAD;0SDcdY2RO4em|qylEG&FUySG;pEk3_B zWq&b8eKw+~i1!rb?0h|2%|jBSU$@kcq37hhN=QuGlcplydj~;vd!H9Idu8-q4m?rk3#fWZ#_OU8#wLWV`OHYDgf@VdFs{g ze$Zt31dbw8v!|n@8_3m;Xrb}=vn>0o8f>W=(C1?i{nvn9LC&OI``N3$XYFFaxln6z zc#CM1wPT^%2i1kF?CkQg?YhV!X=!MDbK;3Khw>L@Ie3vKM=l~8li@hujs~XoZBjDr zJZhx0t3v0a0lD_Yix)8|E!uyAHxu%Ga@+8o@YuMELWnh`mov-v|M{G)#1`#uz=9ux z%tk{cMitxV=(5OBQMvQ-Si!fZVZ!G~Nqw>ERb_->m0ybk)v5aD?5Je%d7;(k zBwM;E!dwS7-JCRj+a4e`M*>Yt>d$dgKSk;2TnB#hR?z*5nrWUEQwr{2P`LJk5>LFI zTSuZ$5&ajWIOu@r;=UTbc)!+IpvLo8-SqfyrbNy2$jEOAEwgo#YUxr@xq0_Y#gbZE zu~8n9G{aF#`l1hk3mpDlZ%fbBX=^6^D;@<)i$w>*df6zeIHSw~_wP)CvNtVy?X(D#m-QM; z#?R8;p)g+AcfW&g();18j~mEKVX#0O+#F1G8}D8_v3tvy!J%NhJj_RWnwaE9rTsE9 zLp14EJ9sJ-cWdY8satgB-?PeAmrSL|0hGT$ynn4K9tbP8Z12lVUYAugt{+Ih(YfuK zn(jy{60q!QmQDG=^$V&ivhCU$8U#2=k7Vkzk~Fm7Lp|g`L;*%C{(LEKY)l8b#4*pE zJ9iMV*KyH=egorRl99QEWLYB};GKQ^?vJG};jgxwIc}RtDHMMeb7TyqmhhYH7u$jG zXk=z#!ME^o$Yb{nddblW+aQL>!}OOt%xr(Td7>-QWDn2;XO9WbnQObqAyZOg{+DXmE@v z@BaZKcN-g!%`wobQtho@>F*E3n zU}-4A8bdWN&Iy56Jkb>f7;RuD*$5unYJS&d4@HT;s0w>f2+?~4MnynNz-a=g3%O7y z?_T@|IhNaFG^_^RN4GXH1A;+_&p_|%%-1S5Hk{e~_|(nm*Jz~zU>oCHxk66>82s5H zb=l7R@knD!%XCEYhF@e@f)rE`o}46BrrdG6)-vDSq!)O3Z?;!+-}J#9%v}I7rM9N* zfap)1LFE80me*DR#W~Y4;cS2zv+9?xS(pH3dKLB>5yXmqx1{s-0{rR&+H(HEe|j|( zW4f$~vma*wIU1ih)0O4cby*&$*KzIE?9OtWXGNLQhzEgzXzB+hDIUqc36F}J?qZw$ zWJ{?%=Ju{=ao<_?oil6+(yXYI(q|iCnhA|x8`kGO(p}gpgH7Jn`iwU=ttJLa_bo=g zDgfgzB@NDc!$xLeXsDC*Jd}DRTtVe`fY25I>-e!dP|uBsb-jHsp2i8^fqztptE_v>+d>*QVF)o5S{2{YHC9TY!_3jo;2rUq}ra%U0Y4({~mZd zNPOh-w+^pn3)@e~|CNmT0N@xn>z*Ft#z^jQGHrbiY^eA;$(0ZS3Uq9&0|6oe+y*sv zGxWmcYjLTmfw>*aL-XDbOP|OiIq1WY%Z0C=d*5iUQHd&obd}AWZ$uiq(*|T|pjlO) zl>^xSRL?-(gFtZ?t1ZiacUh8?LzNRBASo!t$UlHmi6T7UGzn1ulA|h%>~X=v5-b$S z_16mq$aJ!GPpd<~kvTgu-aU_qcu-=6hZs8M_U=B%=uCuaN|}R7`lB$#rB4L05$$CW zzEiO+X<3YE-p9sv8*aE+^UwEed3QTr?Lx_I2A@*ct`5{r@K*;Zy)X8Gxkf)e<2>_-Z1>Fn?Ax&8!4kW{^8hl71ei$t?w@psqe`GvBo;RrlInl;=i8Ei zwat=mFS{Q#7bMr_OQ=>!CD?SbHVjt(HA5ma^JB;BO45g`pC$|BTyubusJq0II@IJhix_bzKLk`i3D8gph>B@=rX^km+$yOMGb=nAz}Sgek>gY!L9RofGYzZyLIXeh6?1 z@{uM{de;$=k@NBZA)$XX^{A=U*1mLHa==`l=s zMFBgY7_9LlS-{d`y2+vQY{pvIq~4mRw?u`dT??;;W&&fRn)E{PE#ncZQ3$_RKk;-S zsCWTdE~Pd!5aci-IwlgR2q}qGgTHHZFT&a(@f85T0w5#I%<9PJ>a#i%2qknq4y=!pjHSSjBw;N-um8Gy(okB-T_dp}#^%W^xH zSPgy~sM_#p1)|~|o4&ERJ&c340`+hLv}}YBHR(Wr%C0%MR^z_-6<_J+%aGiUhT??O zhUE50$ue8&STq?*j??*>3Nc%a@jO!zAocvc9^IfQxlh%#9GG_%CNzFwn_U7aZN9FU z({8mF&!`%+?G9}ke}hKg)#j0MYV1lS7^akKwVPNLsF zqx^n$!yC{m(n$f?MKoQ&vYQ`ZwbZNBNTh0vns3((Gx)Xqqaez54E+$=6rgN9Hiz!cg6ifV z^=E0-a!7!sPnN(e8Aj)YtcE_oJkt)o%_ej+m)TF;F#U@{R9aW-ytd zy;bSNy25Kd4ciP+5_5IAQg&($JeQc<8R(|9Ja<>?pm#0;N5K-L!haS~s5DuX9#_7bm=YEfb5~YbPh$(3^v-R7 z6g=*@@))r9`DEL%<1aRJ6XIx(uoO@HH}61?l8vGWhtA|@(C0H&w$V7>oy7y3I)Ky=XFIG3OU!q9t7hS;z$>}aYs3+Ti`I-MH7(_N^r98L|x%InWj@1M?FN24XT zNW@*As4G!}U#Nb)tf8e!A(9~Z0*N*mYt=oo*HOZQ9^XK$`WfrO3DW4+F2bwpakhUVL-j37GZ+K;5_4_ z8*2Rg*R+VB2dv_QuOZN*`C&ehr6sSwtnuW>7ru>}TNgX7)Hi}2e3jf&+TX6X-UF!} zge820uHb-Y&}s>`;TA^xaWjwsEjiDnY;qKRy>vsd&IoD{WRmZtnGjFGmlh3Z)1Js+ zgd@gq!Z8zci_`@kGSHvtgGw%WJcZ4y?hz3;T*D9y<%Z3q#zw45_{%Y^O^nbn=XQ0u zMc=+wT?w2vE-vo5^w-Vt*Bw3 zZ*wP42rIAd!V#ff$rXC7L!_YiU{c>6snBD^<)l-|K~bJ*aQ{aNGGqU_B^Cuj&;-Md zE1+FM78C&-Ms;bi=b$diatlKNhEYl?`gR|jUj9kJ&D+HzGi02vT-x94*9(6TL~xCw z$b3ot*`Yo4T-oIFumn6@!dS-}R8*Z2Jd#j=#|l7Pu~e1gm=4BHYUF+ecn}us(YLlK zx?!yq7cXDlnZZ>D;mQ9EZ1yj<{a0cmG|kMgHagKLsEYW%fqq*_K}S(0mwm&)zefB9 zCUTW#AU*CqG_nX}tJ8l6IKOk+DH0z25vUY0fInV9XR?FIdVVxnN+a(L^5CU_79LbXedB3iFg2OgYw&PfijI`Q%eiq`pZQesLZ}&*LnZ# zUASqkb`|q{E);O8`JPgN3YMU=qT7`$Y%b|f5A#pnP|buIitM{pc&h~~3?kpoj}&4y z9;9=&g@#-SsQl><0Fe`(PW1Eh*^flFhZz`v6UxX|5B+Cf&za~I+EGuu7Ejym1F>)- zb?XveZ!bng$BfZlE%i3k3FqG$2OgqQwkZ&$F$rnU@tEOAD1!=!MUBKuia}ziD?mR3 z(QTx+cRNQw3Hq@)cb7*3S!n&RJFaAW`SSE-am97VY`Pn!Nr?npQx%PaI}o7V=4j*su0UqnQHdkbxK z7c`h?Bm&^+`Tewzn1npM)&)Aa#L_$8!9pg4A}u+KPSWq>k-Rv9OLJl$rua<)3Qg%g zr#$BkpFCl8ySA>b1m(qN4ZB01{Laonroa6Z>g^Vt9X=*z58q#y~6zIUI31s}K1=`BqZa3q>*KCG{=F9&eR%FS&7#m|J&*!{_d z&pndLP0vZ#xbB&l{&Hw=8Te=z4`nL{q^Y+A>>WD68o#e&4o9@-Y_E zZbTGb`%OANhY5JVdG+ zLvAbWyF+eScB!KVthi-;U2!%m! z%7;f^6KRu9WTCb1ly+|l>%)TgISgk1^`7a{7j|2;5>D1z##pW+?vbQBkxYC~R-3O1 zeEbxdn!L(pR}``-|nG&ZVM96>QqC zN2r_ffU*;SVm><3_;t!-#-F@eD{HqdkYUOb`}(!A$&6~>hUKQzf`!%UYToSZY_Ok` zk(GqsbD6hO8>}MxG<=$%CjBh*r z>gbeCNbMCqH>X2HY#-+<9k|aYqHSuaM@>u1bDVOdHFltV{mMmnc*Bzt@xJwm`L=kf z?d|Qhjt-Xn2Q)ot^TuXott&(J?{&K$^z#APrBFm-qYP1}h6o((fbk>({TpNHV&8=MEj* zTT??rF-w`fy`$q(QPH>h@vBZw0-ar*MMZ)wO-CB-ybSkB0ZHW`*ny-$G| zEE_>iL1FUrsee}1MOw`1u(7Z2yow4gvZ^tC{rwH7f@V~K zJaS!hLMwcWf{*|FMSDB^|Nc)nxOeXVmy`BHHW#`mpGO#h=a`)-1qb)^E;+ZF6`!f{ z7+JA;olwBY><(MaXmc-<<0lKaox5;&u}}C7NnCZcx3JA&@GrFJl`Coe-!f_FX63h+ zSW4cr^6;eNrTEf`Ge-vmEDCPES7yIa4V zbF5yndq>jqG;ca#EOZMDWCz*rp*QE9xeaUD+h&h=MYMPL9@aQghljTXoqc)zJWg9{ zg^m_t=3On(t2pGLDyrX9iMtr5tP@d_VVVWrd;F%0+f{ zxa(j?^W9I?oR2BEhT>&8caQ#<*jvf{3|dUQ$7E+?TiZDpA>S;sror*~@UK2Dl9cXK z)0(77qut&gn$)Xd3gmJJXnj=$W4^OUEx~zqb4kXO7sQ24{!*-SZ zun6@ELYOwWfE^J;kzYSb7_79lqthi-YtJzZ6Mjllw;GgfZEZFCRrCovRib3}a^($k3IEdk~4-PI~#idcWQ|AQiN(OrN;rQ&M!wasqlVNFTG{t9zJ?W&eo)S?I z_B8TUJ+@{pCUF~?>o;<*uG)-j?kIP5bzz1fQ6lH`_3^(yAN7^e#KuOJ=_L*=ae;2f znuOcP=lz?Z*Ju}Q%d|FlEym3vqpmKEd=)5ERvxa}Akh2x$kej&QenVBT4-Sy}f z$uGMqoJb#hT4o6<9x9A&i_^9MzH#jWBq+cvf` zJ^_KO+(P_^1&InVG@q?hMJb{}!rz+yn&f#A)4MkIR8(}BfsB%}AN2jzyfOOtwh2|* zVQiE01>W$50QL0U^E6&2*w4I6iUZ$=da+R`Sn`GsFsq?w|e}A-M zhqU*Jpv^?lB@q!)EZTJWPP9-=M^a&Lc8PR-%}Homw{K@!YKnuS<863>?;_n><73Zd zPU_Lin|l3LC4ZjJ(z+w}u;5yjR^1U~3<#_nnS;w3>?GAOy7_k@410ZfNUfX|;0Bw9 z_2rcbh5W&{I_#LtiE59)ni?tl{=dFHO|2o@6}z_Qs`!zGkv7U+g*aO(-t_R_kfrlR+ej>YY!6)SEde86+CC-P%ajt6*O`kme zX0+CoA_C!0I~Eo~gY|lK#J?6;N<8&iuuR>?zn<&+_p<-TW%3Ks&A)gZ)}Oua@Aa0v zHJ;4Rh6N2|Y|`9D4SnYMn)#|r!*qX^Ejf#0!TqZKQ*ZkFVc=>H6O@_kwb@^KOSoOy z$mm}}Mg-4>%ugTh)A>8LO^W}*Wh}loMmO)}>AbynTaUfc_2iPPYbi!}OCA#Sot$V} zBzd$ys%`%M?N93=`^T1+5%$FPjERXioR^0aqf&g|f?2NOU3E`P3f0Kh#be|yNhM6X z%FN#CnbzKT>>PEPZ{KdG ze?7Y9wESCarjQ99(($M7*QJq6Tu)C=?5wP;4-cyRwJp2jnqcR9hSE5Vf9?vP3HBzYG=m{YvQ?2{k%-?Zp|yA=WqI#$aq=NeZ~B)8^e)WxwkPb z_#NfmOlh|yM0WDt1%-wE#9-Vtf%u+cBli$9y<()6b_cb}Gg+VmI=dF7Rl2Ztl;y?U(L*>k^$KPO6#b4m?h7bQ^`y14@miJ`eFcE90JZeTDZtMT*D=c``mn$wyI=y+cEbo4wFf zDV-f2L#oXj(^5BaO=)>e5Tsuv9cQZg7j-_p)!t2AbNU|)zXqKN0xBOX{m@WXmxf1K z`pIL(l@aizLF98#Lq86Wj^5m|(V&INa?3aIK6y+V)S{lI(NZGZ+$^s+zFb~J+S<|4 z(%u&^Gee5)^3xy|_S5oULBprn9Kcly>gqAHH@pK!QL0wU)G{n;^M%fQ?1>&bevc2o zCE*`10Wg+F3)zo?zqQ{_)Kq>$%N&rT53vm`bWr(7q_b?*% z%A1>XXf#=TQj+?aKd4}AL1=Wx>sx-Y?JqeHD^E=a!nTdA_PkdQP1xbpWOo5p9H)q6 zd6lVQgm&-1fOF2i5u(zr%YZo+d1M)!pG<~q(Pe5Rqi>G*O<`BJ z#OW0(4;MY9jE(JmH`((I4lo3UPOS|RAkl6`#Eyxd{}t@$@`?(6&jDU<3K5xqYbPpXR|u~78X3G0)C9)Mf{vuA#X`?#_h zgH|J7m;(B9dg5J0r~!*alHYp$w11J`a!eF}J)0{myW`NkLZ zS}?V|oI}ulp(EYp|93ayH<@#r!Guby*# z>)Er@k758;VlV%s{@+=8#!x1`ZY+uF)7Y~I9>rEj)%wp*-}rW%wJ$SFM~hv0!jE1SB+EWrdM)GY_Nz1Y;FWyapEtUAohirufj zemlLOP~m*Q4oMJo${$n~MJAT!>9WF0=Pc;qaqycKlM{)~y-6e_^qJMETC%yuwzdx=HS6v|o@G`Bq7hl3Dg^Hvg3Pe_7QB-% zO{#0!=kE`9HlMBr7x}c%0AY7O&}!frb7O$mPY2jF&PYQsGXzi6_4vAgMVIMlWCfL| z^L4$UQBg6mO+7YNPP6_6Z0IOccwG1ql`MD-kV)yra7RF}%BLb;7UyG9om_+D;E2S} z<}242r8ytf!M*$68d7GfpTv>!JR%^Xkhjnk4v^n@4rWB0I$PBTO(uQ`9DZQQDLnjZ zq#00;(@&cUI_$(XuX%6a;yT^nF>E@-(~o^3SQu8D`0cg*4GqXrm-g^y+WHNeLr`=e zgvh{M>oR57xV*Xsio(Cx&G$gT;z5lGSKPZ4wn)5WnMaSLh-DPCYkdt}S4UfZlTQLf z!Ej||nZu;{#cXa^SXebj<208knbMd^nhl3JjQhQ)YbCr(CgF)S5|hU3InMZ~X5ftu zd+v&EsA-G(+d(VBz71(4SOUUy*mxu!pqoEGyrn(hk{<{Ful=R67YkzYrsB zTF7?el`fa>*zdVNSp;0&~;weD6`ip~U z;jZy%r+>;t=xM~l>aalfF03}AcZ2R8wWv(aHCv=x6CxQ}YL*g!Z9UYE$%Q=WVY844 z-VB>g$2Z$aq_&x;OdYrE!AN`uNhPaP0G!n~LODyO?o3_(k(#_bbKH$n-$o;qDS!vg z;?!bd6{{0Ye?SsO1Tj!&z6~}dpInZKOMV&kjAQp&OLTe$yiEIRX^Czi@7@JC@9fHd zwqjSV;NsxmF^De^-TCVEy+T`pTUK5Gv(qc-_b3R6qw3~+4dsqU0zx+Q1^Ne{UB=Yl zje&?agG;SEtY#B#5fxrMsSc*{-@yuCR2G|M8^ECQkisp0yoQx3rgq!+3<~kS14d(3 zd(}P9v4#CGHTpO}CcEc2@iKYx)g$jexJ#ssH)`%eS`V1Rgll`3J;qT}yVm)N?;=Dh z-}}|CwR3vt0FYVu-AY$YVc6W-GSS)8NR#xHQG7x+L>%Xy6i_cCY#TlGd&voFZ@E1| zX%>n6y>TRn7p?F9#8@e_PE6p9d=U$P@-Dr5`4B_xn-?lcv#|zYk#z8}HG)r(+c261 zK33X&#EwY(@OXE0_uY797j2df`>(Ma(vguE_(e7HiU1aeU$D*1&kI1Hfp^BtTR!@r z(zm#PH%$M#u`hIbvkVVzTH{@q>%O zieJR3;jbrx6hWXkY~x;m?=J!9iaa$T12HO*TVGX2hcj-_`p0LBwM(yGQyqYDbaih; zQrwD#Wf6R(cT8<&eoJQ4W=c&i4R4&VxR_>3kClrH8A_Y+XI=qq0ap}njVgt<#(hOt zfL^{NNK%2_siO{b{IBZLV-^l}=z}^x*nkqG^>FW1t}*db!1-YKV(+GhSOsbdAmi-R z=y(3~T8g9;2uS2Q9xm~cHrBS(-a8bM@$eA)ywGXcpSV_DHN+q|hwGR=&*#wR%xx5| zpaoCFgcPhwJPT5z9!1``H<|s&>(eX(^eSkUMQ!V6029PoUhC%TRI6(k<1I(ERH@K6 zgI38WyWZd+-&$HCAk2GgV{m}_8>lPJ7=rsFZUKmba5$esn>nfh7!KTu?`UO%PgvN( zLM{lobFLoT7rJr zK_=)2K1aXlC64%@e-!&-aBxsi2qCpo_?wAa@WL21DSamS@xhZcX&#x@&UR*Z9o8)K z!ek2J*dGPQ2%?4GiX&I-)AHyBr@`~&3)4p#2(1?so-?FfuSh4@JQku;iKS_y>nB#dmko-X}5!|)ygnA`HiSma&Ds0~y#qleALfRvHGR4oY} ze>evny|1O5{NKM9tx1=@3_0{vd5l5$v0b)&YY)%kwwQb}{`a)A&6gOEkUdxQ09g(x zAI}VazUi4Jvg22WymmIcGF7exBV`-Bhn}7~g1+Odm+)2t3BUp1mxLHkXP8qBAK^k$ zfDWQWAWUcpOA49Vny;9adfEs3JIvx=@V|9;-)LJ+)&>Sk`_^?$9euL9&&bF~^gWwt zsE~!;TS)9WqCo8*y0CF^$?@Bt;zQ6e-OXzvqafpbV`T5TI^F>*8CL5WD33?E$&E-o z$K{%efWr>FfBoUg9a)EJYIzKe)v z040B(b8BA37Pg|&1%~p_Kn(c=a$pzGdBRtMd-T)tME30+d{_&9W!To%9KMgru0qKo z2sG2jur(z;f1{TNZIeaLE=!KDeDWML0cC;gd!dMg>$>??-@QhosypBYFFMo2g29^! zt%gp6MvVN_Wn}2j!HX5^<8p{Jb~cs_BsTbameb`?m(^hT%;|kZ#o|!G#bRJ!xJDx` z|L75ou5O~uIF-MArtw&!3wRKnpv~}~ZyTC7CvU5(;}^=OQAs$w0;H<(wUP_rWkBo+ z{?_9Fd%lQ|?|^vyUWXm@P4<`1X_LHTfWB`xNY^kq&*=TWN)-etHnVqXVWEAvx9(U1 zk7774AsOfCou|G)-O-17SiO~|-^5;k0RI`O2h*NeaF;N*1YS)ox< zU!T^*-cIE_sGD@1O$>h_Cb2t@QYo zeLB!`Eq6i&v7v1`%c-p9^)1pij6){D47Z^idcO+=%GbKT z$Dz-_?H)Z!hvv&gUfOYHU4_7)jAISNDE)bI6r@F-KPuw!HSUQ5t6MJ~nvnkXTb z+5ebIb|A;9sri4k_nlEuW!t((8>IvVK|zv$f@DyX93^MTpa_DXAW1SvR-#Boa?Vg7 z1xS)CpdvZvB$6|dGjHzp+%v{K@0=d@-S^(V+oO6|O|`Z6UTdy7zxjRNS`>1a48H!W z(DeDz)s@&ru0eB=axkiW?7(_Qn|7)(Q1s!a7o_b;{rW45w@ApyZbqZ~Sai#SCVBwC zWXj`+yqGBE9CN~>23X_frts=$nlY~IC4II7KJp=Ixi2s*Y(ZS2@s~qr+~LI(m0Lta z>N)A6-`iZj=^A%_77C!^#%T(p(o|Jik53>VFDNXOM0jpbp8$PmkON4BqL&7)5Y)s{ zyo-y&^>99}2#<{1C`}q*Z5$j#)m6Lc7`DbX?r&Z*1|9&g1)vPWoz^=Qwl9M(!>k}l zrhj=qUZ5uU9hdpTz=(`<9-*NGi1={(Ha;`6-2Y7gfMcLxP7M2w{vh?mOP2_sYe6vW z^XJc@FalpEC*OMOth0Rvunp0JEx<2Xt-4&z*R8FCvNu1^1L1*?$m-z_^zh*6k%fl&%8a*S5BE z$PmcN$q_&k2MdV__I|aM*%(o8;Vri>=hxETWs00^I8Q)I+EBi!%<{P6ZxeLmun4wq zCA%v2f4DT4Rnigy{bP}1gNYYdg1XBe>$WHQKZ68`!o9}Yu-~Hv3=|gVl338#&M*)B za;CVxGF0`Hv;M~$*M-r%+d%)_g{(43J}B_ZBNuD(OEfRU@7` zFHkF2=oJvfiwP~yWB-%mo1_maf(d;YTwX0H4UGt_0)aqD=6QYLl`K%z#Ix08ye)4Z z3z2gld&p*_7Va8VxgGmt&-2)?jVtC7x7`7uuV9^5ba>3R)J$6}fsYMEFtS@xa6_Mi-vt?vwM~^tH-n>ZB>HF?rcdaea z)mOUO6&o5|S_N0v5Ts8PaE=6~>k0l-q*6i^0EhpN2Z)I316xa!a8j=Ub#*D_syU2oqe&%xG>@@W&XLlb<07OueE?;qunQe=u z{_;gzU+a|RVUz!|MaLKu};_#vJxY&HKYRq13^(jpqwKW)C~|os1fO-POK3)@!*Jv zTYi4r#-Iuz%FW`Ziewe^o&~%9o*qWKx_z#pu^Ubq+StV9ah(+a-vR(@AWWrr9E8HI z(0AyWn@cowkgd;m8N8$&f7UVX3FRJP?~e!z{SkJof=3lN_m^}NH{sB+&1-DkWs0{? zvH>K|x*Q8yJg&B82Z&xJ=WB1CJumrFVewNx9X)qc{HQi96fJ&Z2LdRwH(2(qh+vg` zEqcQb$m&%b@cZygAy7lV9<4=LF7;mrw)rM6FA+R2kUAld7`s8wv#Hhj4u?}p^EsIL zil-=bf))nxNWI_M;|aYu`)TD@+qLlvKvQfj4xN*+Zf5lYLNqiwJm5=1H5<$_loXJ3 z+~it_-J;{rq3|96(0wFVHng%pcc84IA`1*c*z11Q!Q$uqpaP7_RQf`}-f3l&PY{ZN z+5S|~OUJ|)l;U}K_ACR;sj}XNmf1+yg-v-{^zr4XF;Ga?8c9Kv5IqhbtH;v4L za&IMgPxwA86h{3P=(o^}$;<_g@spU9CbI~Wu(@PF7Gw(C#|yCM0>tLo^inRyOD zXT`a=QnRe$;&6WBU1&}b+=lL%3yZhL)r=a3LdkAxDNGAIAIx5pup9ud$?Z696gQ!T z3Q+pI<>m1FHR6yD*M%GN5uVb)}UBS288 zsZ)3Ea->806GE%MH@-Kk|MX?ho!vP1WDC8|?SZC+00E?{6TT(i00gGbYBML_P+w1x z0{{PFua!u5zQyf+V5|1_u=woES}Y1nKTh{3nkt9J42SQc({scn7IvwqTVpq`*WRL^ zBZYiU%J0O3a}MDrmfXg@An!pzyaL#OsPBWa=er!ue4q6@_x!iEuu*;H=Goa5d6(*s z4@nW-2LYMv>|7$SEKvwel5j5&#GDw8=X4&B5HfufKKJ+g1o*}vMdxy%E53y@_u1wp3^y4NdRNBwbJ81_=(wNbDIMJ>cO*p%%IzJ!-wj* zYLofary}Q}fLvZ3uMEB;z~eQcqJ89%qn=wZ@9T&(N&6G5lG*A#eogPcP4#_$4-Oh6 zVf|tw#lJ^zvvlnStibUdIFpnXGAEL=QD1-dIZr&aJ4jSns$BIoDkkj)Rw)PK?i^6p(cWe%}cuo30)D0A6g+)KTXy`RJq@X`rU&t!iOAAm42nAXJ_Vf|>VEe9@e)u5uZ-@K5j1ITvxJH@}4KmH? zW7SfDWI>R8m_8O2iMA)5n5h8I~(@xL(Fb~djpS3@C^V$>>CDB;0pON z=DhpowH|{xMybSv$W88+`mJ$dBBGY=8PivLcW^~AR8qQL6qJ^V{+t`|Qg~cyL-UIc ztUriM2oC-ad3m2e(*vxB0?@mc3+qQTd*$fDKhFZ<5dej@MR78Y*JTS!RB4UUE8w~x zWG7>XHAGK^aC9hlUK6Tc|8dz(4*X`cKoF^=-(?rsxN{FT#cA6_O|3}sk`U}l=LO)b zc0tC_kjn$iVnn?B?)4LX84>41K^;P*K3Q2=0+u|MJyxId=UonrSx*=Yt{Bj2~^uaMU3hoYE%rrRO z9`BZymoHO3dxm(_l-h=4-vQx$LY07*@_#tCk)feY5FH3<;s+1??&v7Cz_`U8YB+d^ zk=977#4Mh=N>EFX7P#8r{Zb}O) zhhx)JRC|;NE6$`JH9KplmafEb2V6Vr`!?gZ3N*P-J}j1Up;{u@J_GQ#$W_U>D_UaG zk7St6klWJovW6YwVn+q9cwx#@0Wj0T{u}5js2vYDuGRHtl9k(cT!usK(x9C}Tyt0e zJ5SyMfAV6WlnDGv1mq4eZIA8m=&LbE;jMnojODC{My0dXg4*I97uRg?;3q3`NKJa9 zgL+yVyAAE_k@Si-^IfLYMEl&k&nHu7;%hAlQk2s=LgM)yLP47FQlRY6y#-7Kkh4~~ zQyz%t!E&qf0Tc|hbczzPguD*6M^UH7yl|)5m5JXY&G|{d4u-vR;sFwm*JHE;avnwP z*MGhjv7wQXP|GPgi%Oukg~9prq*o201WK% zm*}NV-kQKJE}?C?lZzQDZ_?5P4NW-xiQQU8wzjuJL5{l*dzL=Ew};yukfny>xU`+iS73Z%+x<9#$|({p&L#>FJ}eC2|wa z+)4tT!tg;-0Cf2EU1I#~931I4y+uX(a@6;fB))!?j~Dnu^O})H{zET+f7rFGF&SrZ z&3ZvrAPjUhK*OJGc@uMgT_|4P1zM>S1oQc5a509%C&-ot_XsgxMPT8+fjJa(y>+k` zZmp4jAAxjF0RN&W*}cyE;UO(k-WGFjZU_ipz=^H#LZr}L>g(TDg?hrI&b0X$8c>2D z%3;5hlp92hS8>2ajOZx=qVW8SZHo4HF z&D7l5`m@8@wm|BW)f8RZQ7mVE)T}w657Uykwk2m5S3>tNpNeKo2 z2{T{{7M4~kJ1a>ZP`-(-^IMVCfqq7{IJ`{`2G%PXDhNV{^Fc!r$M_Lv63Cgv#Iw-L zlw01wDH*={`vod0%nc(OnP3IOR#zt0rL@uw$%UW2qaJ4@A6AFYUAm;Azt|;!^*7S%U!qaucAGx(W&qvn0rXdYb<4A2eO7joXH5)cGk7yg=`)eeF}igLqwu&tZ{iU}YDL0w)DbA3ea zKuCB*1iNXwOTWda&kv18q8qnnxc(8`*V6P6$C>X(P;5u#+X|;@!PF9}9jN%D!x{p{}g8tgy8eL9CZRx5Vy5WxbJdJ;kQs&@`J0$}#Qu7teQRjj7xWj;~~ zihcrDAXq=d(P~Iv2KzkIjdt4sY=%We1w$ykfWH%}T|*cwnO4{Lm9!3+x?^zPXtY9JMYkXNeXT77!1vR8TxM5Fm6yPf!XjRj?U#X1?n^ z;t7YTBFxihY>6T}VSqoue}H&9jt|b?{BH&x;%-E8sq)?dt9;zAOJKD9H}@_S4!{*= zJLvi;5jaSvOKo>Yrk?<}AD2L?<2OerqDY-l~m;&IkFIpO`JR;I+qxTW4 z12~IH^e#xr;7_>YCQ1UUF**4M?<9HYZ~LO$3BXM=p!b+Z9F|j(lV88)VghA-KZ~LU}ZR3@k7S z0|5+%MDr>~)d#T6K<0tWEtg|rK6_NMp#r^-nwuN^LJkZK7(-y2Ko}t8nm?p0ZHpVB zuur!5O-1uMQ_|F;z!ldHE2!}4lt!XJTZVdzAqsBsIk1X|bO9$Ce60ilcM6uOu(!qR zJw<$A=hhp3&Q!Glu+!p9kgiP<$$v5yvbWE91Fmk;XIqbGYQXNe2)VFSJJ~-s8N?c& zn{K-I@L}|3RdzJ&m_vvU(7YaVVd{|P+uL1LTPHqdv+bo4rtQA;KB1zI9D#BbRSSO zwhnC~`UFglBsdY_+^vmiV>aMjr^En?HgvIjJSv*I=Q5~=(9YoB2jxxJbg)1~T=MH( z+r5Wphl}rL^;pfXTS2>iR7N6bLgFb*)wXxxsqISlo1-I9jSmNK((mXCNJ;B5d9mP~ z+i2MtgM9)$*`f5{h_T+Am+{g7-7J=K`>D4quzSQ-)nbnk6^-1l36xw!l0(vf@bmk% zj8pWuCeZw!CZ24E?tV;9Kyk_0VTyi{hSU0@ZnMF7Djcr>+YYYd+eQ_MUBz%kJ4qyMyuy zW-r;wplWD2o=>diF+|9^wa_Q^iVyCzJOEc0*s%*8PM-ozX8?4n1fUU17(%ZOfiVK1 zO-8_`!9x4NgB2*l(%+M&olJbE4Is0xW6kfz9 z3V<~XVZ+_LRoWG>m6Y4BoCkHuXg=fwY@(B5uIwfx6RZ#GK?R)eEo3b(?|2AYqkAMU z%pgk_S@(4axW;wZB*NuF zRAz2sMdaSUFN?B9-yBmdCNX(vUWdC{|1wv{+_XhQ4KF*(=y8eCds4PYT4K7P8u#gV z3PnOU=eNumNps~A?nhI;2X(aq=tRMeo*(&_!YtN*h*V7fd=krV5t=Dqq3Z2@Zn%2R z$muxBVm<}iu*HgE$jRvRjG*dae9(1%4DRi!`?X_-{MEZBYukAiLE#qPf8^RWeXTCG zY{Ch2*`$kz+!+|JwlHZ?<*cwd;<;3@XwAv#nzQus$VW6~)v0uck<_4dz3t7U)mCtrjDzqZsmdO@gzRg{7{kr9KzJ0 zN*hK$kv$?g-`F9LwK`U@kgHp*W^puIyWPO5xD=Nr)l4EKc3w9_j%_u=TTbJ}bR0Lj zxVUy_)m8OzVoq1WRHJdHdQ^+Xet2cEi$nxve61Sy*sHw7#l`LUgT_80VgU{*)y($J zPwt7?xtJ<#U%c!67P~c!KeUOiS_a+y7TUosnWxRI3VnYk*m8>vS0wkHc5B3QHCfd~ z1a$xEqs*~;%3~EX^Nl4P3){LiH*`A^=Zu?D9V;de@^jS}UY}@{==(8gY8d}&2>#BU zoSrm0?w+Jym@Qff-2bfHCgmr7RtHCs`MreyWA}# zX{F80KJ&YAWiRxM-AgK5^!&I^f5=2cTvC2qr>BH`K|=eK;*pY2$EMoo+FgYtu15pf zHYHB+n%NqObLfg36=qF!Q&WY!(*9m1cp!)-H6ce27g!ovJL_TxlaX0=YXLa>IJmgQ zR%5$Y1E-rDRt9_*-zz?tUobB@D04r>-#@sv+ZQIWIn5K~GN8k+7N`)IgC4?o`!mlh z>5gg=*%&bi3)3Y{Ws7~|*_fGrmJ3&fQ+4`t`huGW=DNpY9Va-{)h(2m8$&r8N%m7i z1q6RREU{m9k*Rev$ggl%%Z!Nd-&)MgH=P(+n>U+wOdLLF)@19CADZYp2#hP)o0OU+ z*WeQ0q>9hGPo1nWF;SqEo7-=?o0w!ehg&(lWMgch?rMQ{(ov(;g<%zT+OSf`WOoNA zx=F&gaC|T;K!O7Hk@itGDo3$@Q+9s$j;*tzgt^46fi6#HH`=9|jj8(&qI-5TL>U{G zn8`nQt_Af3UB8`UIAXtlu)UHdfy>m3ME_QEZoctBwaqm0;P^s!%{tO6C!G43QRk#(n}F@8Rn%v9NaKyB(9mPc^kXb;L-6tgD#+r|m zICpeFdU5c9fXs=oq9}PDkQPK@&}(vZ-GHd0V}LooCUc^CKa#C-Sw+WPCuTKv?|Onl zW~5@cL4ey)tIhC!J)2`<)&+-g%tn&~I+p~Nj)cUu7h}T_TD4E#_hX#pYo1&qT6ps= zrefor%0Ap)f1N|4abwUm^&<01m*az-uGDixCb=%nI!-fv102WWyc^rMS97tcqBG*- z&GrQb)a}$WVEc1dYzg@cGYd=S@}nNRmbc{38`vdloSWA)?#A>&SoU>xQct{%v*;{F zzn|U|Q0!BW&??W-dTeS^Qj)1Lsf76!M4GYDbYdNq{0&S{m|XTbTS=_N~)@AC&FB%G4`5Em-k@9=*)4V2Sx#w-qwI6Y>>##ev(`G1{4AVIBNfaf$g1<`Q6irW{6?XS ztPE!67FDAl_YJ?ZbjcrB6cf<-iVVOj#l(;v+@ugXDopJXICu6;NmgN@44lfOL>rb> z3coucYpn8A^>*#n2chFF)xH7x?Tp)BXLy2!dqzi7h24(|Hw@TGr{?8*`{={pC*-xZ zUbLf)5lEHiR!v-5EQ0f_Gm49o&Ek2wk|EYTJU)5UC0Eopz0V4dBv#Xu%TUS5m1Cd` zM$P;PnoBO!x;2U+Fi z5>TEaqh^@G<8m?9i@l6c*z8LF@i;sG`5Tb$e)jY2#%7u|h1817e+8cc!D;{h$EP#v z9iX?}=!G4h`ggx6Yx3AFh9k48C1#KoOBQz(&$loIvdIvN@=D7T9N>_Ip`_ZM-x zq!S;|Lx|A8rplq_=8f=(88kBx1lx`%th4VL4Ho6g;wcgyaD-*`T<{fJ#2FfTv?wb) z^b2gvx_Qs>h0||{VKf^2&I_K_w;ar;iIG`tUOI5r4@i5#k=kQLAr*>z|78>dSyRu@ zcN-mu&~|YeN4|uIEIB{hNodeiELK?2GKWkT_cPG0; zjE6$S=c~Z4 zq=pRwx=tlvRbz-Rp5Jy{*Vas2%DD&Q#5ctggoE?*@Augg10mS;LR`lfEYQQd{Ucfx z(>JzHn!g^*EBbDInWdj$W@IF`#9YAn+v{4@0lZ4dO8b>AKzKQMBAPlFivWrnf7`1n zf0?B(M zo;VIJu7$j^iLNAwn<$*02U$<9F>;qxcG{_+!*^Dkr39U>G7&aivE+i4#rJC6wfgYn z5XV>az`DyD&S<;*J7>c0G>3ma8JK{(i&qKVS1dWAi%{{wHq!D~qB2^en%MKK300UJGQ{WF#JlV?^~l{uk3= B)?oku diff --git a/test/interpreter_functional/screenshots/baseline/metric_invalid_data.png b/test/interpreter_functional/screenshots/baseline/metric_invalid_data.png index 795f2f7c832f335a1a1d78bfd9c820e1dca976a5..e0cffd065fc4ad31a2a7f09d7f58de66722a83c2 100644 GIT binary patch literal 1806 zcmeAS@N?(olHy`uVBq!ia0y~yVCiCDVASDY1B$H4iCDcuu~YQf??lv5P84^L>lmdNFQbpc^O2E3Xg`#Xo48c4WmT?s5Bfc9tTm~ aaE4jCpDjfCnrb^JYCK*2T-G@yGywpYYsB;b literal 1920 zcmeAS@N?(olHy`uVBq!ia0y~yV41|gz^KE)1{9egI&&`r1G~GYi(^OyVv%M>FVdQ&MBb@0DHC3!vFvP diff --git a/test/interpreter_functional/screenshots/baseline/metric_percentage_mode.png b/test/interpreter_functional/screenshots/baseline/metric_percentage_mode.png index 580889bb7deaf3f06eaa5dcf06737370555241f9..14457f0a4d0ab750bd49fb09b1c1e946b3611365 100644 GIT binary patch literal 23705 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�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%0D!`SK?hshr3L$*aagIV( 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 b&~h=8fy36? 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 diff --git a/test/interpreter_functional/screenshots/baseline/metric_single_metric_data.png b/test/interpreter_functional/screenshots/baseline/metric_single_metric_data.png index 916a284433874aa33d618c30dd8751b32819e457..c4fc4d3979152affbe4386deba159c20c9e5c5a6 100644 GIT binary patch literal 22004 zcmeFXbx@tbw=I|;0TKvq!4DoBLU4%S8X&maAp|G5hTv|&EkQz%;O_43?(P!&0Mq9u z_s*L)_r0m9nm?xMQeUMG=j$}xySJ>hb_acsm%w=X;_0JDk1(VpMU@^sLJEHL=rJ?u z6Yx!eh5FA&k5uZUMBjdNe!Sm;qCMf)+O@*w6&@LEre1SeTd|QRX_rOLJ%UG=L|(3qZlBC_=eC+Rba^P8Jf17RLjs%gxyB(hfG%$a?6kDwGN-D zR&#N1JPj`C-Dd83bmh~9+x&d>BxPl@)Hy_?<6nw}?W*MJHIYXkJ)wlSZ1u4niOkf?npc$H{^76}r$K`OnhLLn^ZOk_aBO~!#c*HgAtcceW<~Eoj z)K%1^+cDJ@fK!E^(<*Y9;rJ)%m1p^5rv()P>?qh~?9SIfN%C3zssP3@sO`xrZHi*9 zAu?2!L}4N?_5_cZm^hS7B<#`Qi|S){y=xxP6)-QD{8S7sIqm58Pv3qN`j zkI{X|mZn|bEY=_4RBa>+E89PLecCqf!ggl$_j8J3X_kmGT|;x*W6Td&9S7m2+sA}9 zlk#)o!FY_J>uGF_u%yNnpA)DPk62@oC6qn6rxWZTVWLMS>`z2Y3XG}7WJ9Y+sX-I}^u5vE8V&t}w3Wv7Wd6q@Rb)nIFxkLF zzM8F{(4x5Rr=?s0kvsd~`P0#UDfMLG;2by3a!iSE@-BP&> zjNqi~+P3uE#hUKlce6Mxx}cCuHA(2!+V61wnYG`XtuS9Fph$n`UhTr|DHZ}2I#*B6 zM+u33#>vIylC@&wesLYYUde4PLMgl`U$;EDUP&gfwmpAtR;VLG&GSdM%v<5oOMdS4 z+x6K0;)P^O37;dAR;5*!%H7Rh8Xhu%dm$DrhxOiAQM<88OM&U*x6BHT7%j?oS8GGe z8kMAK=eskh@BE%p)tGjWSbw?L=Mxlb5*D(pIuyD&y-7ZbOpcC@b~&%$apqlqEFI4( zW|b`Dgq_(b3!$d=ng5=a)ZjwMTWstexhD`v&GKr8j37*sYjkX^he^-t zj0J<-Qpzfc+Z1~!BI%7TJf=QP^9I(ln-4qq@?6>6$K64+9Mi8R3NN@1P8_F(TI%aFQtTrq6_w11&u|0Tbt4=vMYOv zS{@x2C$1Z!Nj$Kg%N9jKZgxc!&#Ff!;(oh8#qZ?t0y}EnkP@;A9UwQC@a*mGUM^83 zv(2IHi#zK{^H`7X@0Z=>`7C9nUTVRYzCR=3+0);j<(U~5xW#Yxmq-M5g`2h?qi|9B z3K9+bSmgOdumgBx(47ql&&P(z|g z&_zY(w?}^cIYDZI*+p$IEw++9Cll@so>GfmD%7bHzs$K=kq>xIVbZb3)MGtWsFQiC ze1|pk*Z7m3poWqXr%ymw3+}6uO7M@_U84PlQblydkXsn2G$V=23GfMPP4#n_EZErN(1q$-)}459peRHQka13Y#Toe{w(8j z3gO;Eg$qcU$#h_eb1mJ#>@s6?bc_bgvXBzx}ekyCc-d#bqpa1^6ErQp)yO?8o%`(um+Kx6#$JXuGI8!enG=CPk>eoH> zIfEihRsQ~%+I-4#ea;$lwSXscabw2Rb?x9l&Qrab7a-Tg+ zN?O|RsPV1<#58cOl=JbiJD0O_6W)A>xk29iPf?J1b|_IqMe*swLGWDL7224Tw0tM= zoYQ`WS&TR^bO<8OpIi-vM}^IO2=5-u4+)e|AFjjt zHOJQlZiH>@SD676o{?4x_K?2)nvg)<3IbDfa{eU0GdA1OJxY?D2C8ag_INhcIcc+M6J#_KEyA!LI)hz9(ZN)w^Q z#YI@6I0l75zyTsF4Gq>fKT?@^cv>JhQQ(k7z)mggJ#2=P{uM{Z@B%j8TcvB#T;{>f zWG99<=v;3}m@-nQ#z4X&YDDye=M;AQw%sbYg&0I+028_O9J1-(L;T)Oj1eOLmM}pP zcdU|RLz_-|w+pIkYYkDF#Kc7BQb)4%c4F^ya)^~Fvsehhig>L9hJnI7K}ytdADecq zePVt2&^$?f!bc*(9jbUeSZ)4z`%7LL~!)o{r&BsE?<5r*V;4oyWZr|n~fi1 z_p86a4$=T|Nk>hMSXS$_C;M>BZkLzUB%)rNrE(vl!bb3Ko-?*mH`n=v=FuGpMgRkaSt%yuxhPYj^@ZqSvw&-zrpyiGL!< zgTR;&PAViXzu;z#A*iu*{Fc|~va4j?u8(DXe7uM4_*3RhLM%8lwW448lkk0p);gGT z>|VFr%X zG~SlqNE^Gk+B|v6iE%_@g2S98EfMDxasNxj3|_WSa(|7w)eFk&9J4e1{_WvL43`5x zTV;;EzP^xdHA15}1ETsmJ3i>$X`k3>@0h`mVQ2GW59}e^zuUHUyNhn0mwOudj>(`b zcU#)-?(Xd7hx=xDFF&G|-S}fdsv1h|9}k|MpZ|R4cbZV*F!vl}){Zx#?P3%bFf1;D z!Q`O)Bp!2FD}J{NQm99{3-{nISOGx@a#`h!qv7OC*%td==0Lu!YQTkMu{v`?#Xu!) zk6YzIousrhB&Rj)d3H?&hl^$3r^(qJ zFO<5p(!mMs3sS}!d)O+AU$pr0@xByZOAii91<0(wy^~8g^ zI~}t_Acyc`lp%ED*zbI`f?_+tb?0W(8H7vUlY1cGXrLOhDQ7zYPY#n=jBEy-7i4wp z`C@0ksz0~YI&qkfevgl==lR?(+VA|!i^$~wRQCiG;CCR%0Psfz9ZYe+ab%N_fe_t85gL2!e%KZ(Oqu&@@uKd4TFD?S&U zf(u$1fY%ZER%|;F8frzlVyBb13$6y8ahOw)6m+sl-Wt{ojp%AFE-tpfWv({jL!5)n zxHk30+efbhw`$z4pYoZMs2_ja*&$9?SyM)(prFvMfK7CaTaV*#(aF0+VVEyb(eF;? zcI>KHM=57ZFTB-d(ysoOIZt_P4raOSUNZ6#bAiHf$Z3CEmf&!@j=BC($&M4M&P*_G zvb}7|vA5ns5m|2wUXY5|MyEV2xWu9jCZreH52`X!4Z`WL4d{5L+gbnsWF(1_va&{% zCoTxehu7r4wMe4okI#9Q3@2r{=+~b6J;iS=7|R)oR0etGr~9?tcQ(E7(~UmGMt8p6 zcs7aa(+vi_M$(PG_{{%&xw5wQUN>JeoAck+1;ik2@SzfCT~u8B2^52ZAIr(5R15Wt zkhLXLP1nt5N4e`njgym;-TZ3UySguC;ax(L4H~rmbe2Rmo&R}{2bIF#_Rx-Jrive4 zSx+K4rfHr`)_WQ#GPUaStKEx%-ixM!u3ZboZspJDnsN@P8dIj-~??>Du6$tMp49VwiuCA~IlpXO*|)P<>@ zD#vtU*wQy;h;f)7)P@Pp{uWMyE;Zg|WoJhzFD?QEduaStH0u$+`xQgY%GjM=7= zR&&T)mDUSioeRs$?d^8c-15#O6q%NcR#s8_ryGq30kVMfg_^CIjnp$xj&R}%b;_CJ1dLh&{13mrwd;Z- z&Zwxj@?qqBzA(qDZjS!ri;K>M&r^*QWfwD-YtQvOuQ9d;Q|WUwUs+49wEAHWIqYAd z!SnJVU0*Cy5E7E62Eb2h_3TzJF-1m0^VN!0gVPmKI}17^Ubinu$NlXJEiZp1*s~h1 z_w&6Z1N1wqPVmcECLkmJW2xN}`HTe`w~wLy=>k6UN;wyI)mi|3aqQLk1lUyZR~&UM z{3G2ikqP>)hXZ&Xqf8LuCr{?X`04Ya7hBr$+|l!u=N_#n2A#k-*h7y0t{OJm!`KBZ z-)!MX9?m}3gBemBz<2$n>h-o&n%qa3Kcr-(4Nu48QBliJq2qN46$S?a0FWdjV@Dxm zDaNQKINTo4_ONi9F9y)_Iv=w;?2Wgkex9tyRIoW-HUL@2uUhS_Yu2b!ehvjs6buXb z$FRcPDKAuLf$OD1`x%@lr<`80z5O&~W7jqsDEXY6zrTPmM}IOZv+D$j`=}`%r1upm zK%v&3u(7dS9L)F!N?NgFVKY(!-VmX|HjpA}Rl8SM>ii8DwPoS3HlI0dc}v+8>+;5U zNSIZpQC7af64w`i6J*8fy51Eu`BeTera?W)rQw$oSMbPoBS! zvYKtA4#%eMGMbE{?m9-==#5=D`T=n7{#+RboD{*`8Fh~<65tRc|M+;q$zKuC%DK6@ z2`Ysa#DXn}T&6WqIq#$n5;By8sBt4d+AO@ju~mpZ=oG zya*5*GC*vYuL0Suw3-XjV4)~e8eO8`!ikAy_+9ub%Z@roN(%3znz9o1<<-uXGAX~k zZ%UqS!^@4{xSy0vO4aOXlmO3}f~0`m@R@~ArTUnq{L>;Hwi>tPVqA)}taK~xyj=(z zRHx1nL&1dT33WP!tg|y45VnAt6AXv~=J{&Db$yd7B2|Vsf!mF$49Z3m(qU+@dZma|VF7nKPAu$`H-NLP z11ZO*Gp#zp#vkrgvFUn>R_$F~0pEIuiWpXFgJMh#JL~zzYKXJ4eyyUpyFWFUyunD8 zl9Hm$sTG+)wzjrj->kA(lm%K#tE-?O{H$q&#Ln04PP2q4~18_p657*b%w$%UrL{(Vh%IznY zTotM8=_v+>)>cn)8FQV#8G3pqa68;s^b>LZ7Ew9|fZTWvsE&pr^SwY_k&=;FdpJ8i zHC#%wOsFtEAyqA!Y3`5TG*bQi`K7S1@O)rV5oQ`CEv;Wu2)x#+#W(dVDpam+Yykc& zpaCpInaVHH(d2oBZOZIrf=zpOXFEyeG#{=ZCDnt+qUj6U+*12aQnMC5$HBzNn3y3J z61fC}1w#tWMhh^Y)0G8VP}_@nm&3+7{_8)#^%VP24~H?h40$?0A@aF7|Ma1WT$w(C zcCI5N9&ks$ffB8Ml1`(5N>Y(~p`$;VKEz|tev{rTj`?mt7+VxXQXx&L0#0d7F(A>A zdu9<+ST9M!6E(Ii;WG$?Jj12$0ug>qkwY)U>}jn4ttXsfV$Xyf#dvz zTL0lc=BR})IADAkMw8KriBj^G;R~(brHrbhKs9d0O%e7WPynuAOMNSH`^{-dYal$; zmUnODR|NPHUhqNbBL4w2mVYc4o#J)mkGO$u%&u^n_Pj{Cf6KQt>|2A*xQ zX}^;PxN)ROkIjlsA|kaJKEyA@b$91J;iPvIIdFa4&01pBO68?mD=w*!W6HNppc=5> zW-|hpD9}55?QgIgN;ml$&eVa|2|q(Ew*zP~h2yHmrq*#s((`(=*{NDfQ)c;SH$8q1(a%3=Lrs%HcA7Q5Xj3G&TJt&o+S`uckIwd0k{YAtE$mr$Xr zH)Qv{9WwFLb=D$$DnO5R_&g=rUI0{i{mv32`syek${S3A0X9`^O*j6@E&V&VW(#@9 z&~qL3Quib07MGn zlKD{9#%?qMBVhKfs>}B9N~Rz=3~5mF{BRFR@MHY^C~Gx0G&?J6Ri2SCH$gE| zoZBu#$Hu&0T1u)T6=;_nnc`ue16_8AiRTMu%RSM{_R~GX1w3jWp7p~+tO$sxl+kMN zgz68nwO5B>iGC-LR@UT~Je^R>W(CQ8>D{(lY7;7~7U)DpwM~HUNC44|>Tj+mulZe4 zRj&F8#rm%R@`4bJX9O%AIzSiZ*xSjejlzW!&@uvM&EPs3T3VyYF$IVB>k&`!U;1i% z*<^)?IP4OI2Ga9++%QAGGbpAs;f&^zU7HQo$8+R^7LCKyBRQLT&{KAp%QbxBPmvA|?!a8O;*io=KkVJX<04D)m3MuV6i+T;@ zWv=J$IM@oOJ!uUIirIEz_tHSh=GcSlq&kiFH;(`+BnPnHZsy-YmgTh#2Aqr)0Sgn} z=A;{S0?diWv>A|JX|W)0_0;({>xF_yS16Mj4x?&6rkdqUEg81c_FN7qEz%4rtF)eb6SUgVBZJ!+ z)?Og5lh&$YK55xrF!@JQzth*2nj>F1KF4Mv$$^9;-yxqM5XR=w>Z(8a#YbW_@2Kg4 z5E}c^fgbYEcTg4I9tt$lW-@r@WdXpfrNH{RdqBSHrQ7qeCm87T^m(eeqMm$SRF6SF z3!Xb7Hy}Cksh#*-P;d@};+S2bquDYBlWa}e z>*Kj&)rOt@H3{df+be<9F_yQi+9TOoY=FHxkU|cl6A}XR)6frmh#MP)*T-A0*$h>S z77=Ya;G%%5>yw)7w|8&)H3_hT1bl@jFw64Pih=_J^MRmk_ja#RhZo42gEz)g0soMi z;=8m&{54w0(tr-^>TeVW&?J~O;oNzzi=C;uErvH=V#ucd^z^h9q=P6CE`xc={ukU( z$e^p)Bm&@b9QmrCXJH%vqf1~d7?)n&4%RN!PsD$vVVBn`(z_Nq!Y13Pu`=|$qktpR zKUz8VrAl+b22+M42sr%o!-o`rGTHMz>bgKJjP(u-Oct6g=LTe^OXH-j>cj63RHtKE zXqmR+XK@D{=GVbhyw2OG(1COTL{r%$_7|qFtY^r};)QST#|VmfvhT z+ag}IF#~7BEpr7-7u5qHip1ES`?^=El=5hbm72eD*IgX$j^#buKYl+p*}d)~~qcdop9jc!Ms1#q-(i7hKHm(CY&g3V+j4$uVF zHPg?9I(=2ZmEqJ3$k=v}&AW3!0UO>yqF3ysQ6!143|>JGrimqhBF(YZT!rFNzO$A9 z*SPw3$8H%@PFkxO>nmY$c6so!4A5&Cf4Q}f2Iwe;a3#IQ+=T0R)UcVKe-Cd-A&~1H zrm@FB`h-`ro|}suDR^*GQw|50JrN<4|8z-zr58>n@V07L7&JInL)GF~epsbyRzv=G zBP3f+_Peh2=U%|DF^NI3@PAjr2tl!f@%jK zNiST-?2GbErE)exWWkA(3ox}umeYm>ILc3>MbdhY$XT3qb|0;ElB zQ4t%QT&jTbvIftE#LoyNifwF>G=i60nHsCE>_8dR-q_od0d*3qCl@ID#>c}WywuQh zR}h`_G9wvcjGpEk^67jsKr<@b#hpZqN|~;vN+MtpSPtboBj5tv1gJHG+voO9am-rbT%)6-pQcu%N}XL?6!XSYLAkKqkcJc{aqb|e zaNE4mgXc!xy_|G;4uerypx^-NUC>7H~Lb9+@3oH&eafObM1<1s>WXa<{R9d$s_?i2j@H; zf!Eakgp_vg>qPFc6gU9%s~$Uy5NTKL_cVJ78Jvc-yc86xDokYnIuB4zO?D7~N|n+q1M!|B{{msfI3Hn~?g!nXDjds?o5Pc1bz9k;n{tDz%IRF! z9M3ANmaw)DJsgclkW5I3wx@!Oy3VFj_%Vu>ZHTr>5OizABS@vKerxAKWOUPHJYAN( zXG6*P0LjE-)lR^iY?AP-IU5vk20g04>Q-`VYLN!A^Oq|5s-3B-jn_}XC5Si}&eLLe_qXKEqdgK7=+DQF9)oo5D zP)=*u<1RY?b3D~af37sGUiIwWZKnEONOZo1+2;IsNB>Od8$-A1wlcp0B}c6 zRk~OEKgI7|v+4+03I=T-;Oo~WXPrdf<|^Nb((r&5kYu>ZAI(G|TZ)y94Kdw7b^dgC zHYdze{nHOvD1b%~Hc{r(p#;VE*tR)_9Wc<8V$jS&bukz}dmre2U?Mti22QCn^o6u8 zDjW|ullVO&q~l@H;*7|R>53)sd84Z zgSQNd8ZVxt9fDOKFC#kG#2OI&Js67_|NgNNwo797ex}r<*iu6wku$qT#@}GLNbm5GFgbAcPDxSZXM;(37Orz93Pt-?vdbq+{@t}7rZ3VxZLo(UeX2g;Nzrny9 z7#OJh<$+ez19}(I(4hj9)XX%Bcc2FYFs6)d;&ZU(KG4AGT+c1z zwFgxhaO>~Ko(f~2C+eI>EfK-tvdjFOjQ<-kP}*thm&WYfGH@Z+2jIGrY>FT*94LYu z{&Q+`t{IJvaaBjX;LaNXaGi?G=LDb++y=r1|Bi=HK>YE! zKr5%`_$OlZ?cuI-hOe)lGn*?0xUaDlK;gAo1ka%a14&q;j0pfyQ9+{h{&AB9yGlk{ zdNUbu$?oqq8mnI8J6Bi!`}-=z(sj%6LI)90YchUF|8GXTYaavpP*_pZbzGZ7IQ)JR zXf~WQ{Xqg}_2o#&N%`kH7g3s{yS)GkKA=X5Jh&%;9IKpVV*tCe&it4wfCd^Dps@w+ zcnJN|g20G@f!#}KAiDqc15eTaUw;G(IQca8t)QT;+lxunKPqNxxW<#%O7$7Px`qY> z{WkbRDJY38bqE&=OjsvKC7C`%P1Laus|OO(-93V1g{(YGbH0?~miZwbm$Iemh<~(O zP~6;^A!(G3_E>99S*LpScc>(MiKep?8&C(61)Bn{2>l2>g}5kHrnwj@)@b$=t)zJ~OX_#h#Sy)`=FLT~N{j zTVUJ_7{65)D#b9<-w=)OKq5<%T)#&G1C0&_&%c))*S+7{#Nz*gI}@y89d6y&-jy?MWH6&5Pls|yj!{{<}lY?CDr*$p- z9sZIkeF|f1lH!w!tRN&Dz2?#Ln)Jkb&!^|$?;SXiG-v@Ot|`Mj$}xSBjv&=w^u4Mj&Yyp6~;CLt|J8=m!q zv2M`3oz&Hq+~N@yoiWT+^xgA+Rcr@lNUPxuI7wDGwH9Vp$CdHn{GU%p?7Xxzl3lmf zi!f7_jA|}A>@|Y)>Hl;ziu(myL`^r3W)Nz_OnR1~u*4z)#~denGXi^ASh(4R{Rsy!K$Pn zOKl-ZKB-Sje9RtSI6Bzb9PcO*z7g6oSR!#V!|0-h30g>NTdm#Ii-qH!IGs|MUXHRm zqcg%frzqwIiGoik0y1)N`K?K2q$jNuEZQmW85}3Sgz9;GwRpVHS!!&<^)uTCkpE+s}j zzoD6Vfv(&CoY6mXJLBxb4ZLL4B3LiCA+~N{~JRyVPhgHMoZE?^k%3i3$ z?HO+uO=-@?nsr7(kzr9X&d}xwe1XJm&s;Yr_oVm-G~On*U{xQ;b!rvGh}NWXF6tCFdbyIh zI=W2lRSXN|@Kf3(g9tL2O0x(p4YlfL7YX-~cG1I)U(|0l4TBAI_ix@JA+fD~m__}p zeznjYNm==jAYIvly@&SapzBCU9>@5g!_VGx?frL_xEEqq3)gpw%Ly8dk7_~i?0v85iiBah8%*@= z@Ps~@n>}GXI4p3ElUR?^QI+@ow?E8FW|?tp&vTKGB7TZ@m57E}h{v5!sTPE_l^lgq zCVS|jRxOeQdx(LkJ0cg2?^In~wY)mD9s^a#sW;%aItjOQ^R@NlXx#~P& z-Ej+1qt&J0h1ctEX-Sf|aucU--1v?31+>yuOF!{zVK8&JST36z5RtvGZIYavhqJsj~$+ zQaOy-fTckFk5Ta)6hZhE3j&j#ZXZ|3?3ac3C}XJ^T(t7dB^=(U=gKHk)P1AaxP12L zkchYH__9os>n{2vdkSy2IC*KXhN}rpNc;`bai!z35xThU;VKi4P~B z{;JFukNc1x`!!N*+UQ9Xt)m6%S`{t(`M^1n$|dzifX=IZ!6?cj&i3$Ji8u`^Rn^o} zYS)l3>5k`+q{1!fO66gBjTUB*y=5@RIgL$;x{0yWDM)JyUz^r9b6Q< zWMm>bRY#WO6N}_d7`JEeH%(k%^(`h;!uTg*f*s6(ivcCqOtL8A^ z(>qfuLmN@_pA~oBq6OY9NOFY|`|aY!L1ZMYSC(kZ>i9A~r@~q8O(Fg|^f>k%5!1Hk zSIRpmMEGiiS}9?MS#}F0poj!#LAQ+|D6b)X<%e(WOZ@dCA|AVc(nL~itdBD#sMxAy zqwa|=rN{X+&_rtkA^Y{bP=esLeub1jrTMo58nf9}gqc$%3PH(yTyH^_d$%D5I$eb! z^=hSTCt+YJ{$Qj&Q=il9?~Rc6FCVuU{T{JpR-{gjGI0;YH(#^a7ohGwRsL)9Zp%Y{ zI)usjmlSffov%MjE86yFV^>wg^x zIL1t@cbavazt0@JLv^!7HjsN68Qtkn_O76Se?#Ig0ZD9di^6Tj zClQ8}zD17y&ua<<=4-ki1n}HMi8NHhP^rdW_zDTfE!|be4R>=+uUg_ETF3-&eSU-5 zp*pjzowhd_h6>fxhOjk|gj8f;GUi;27Pk?-NRy&vg?wMiruf-Kat`QYH|HNWtY>b> zW32xV;-y$$g&CvwHuCh2^@U@qsbO^?oYI+p5=uK)`)t@nkw|0z%>T=_4MOH5 zZE;qr$ghPNa`kpQt(RrwXH+tujFd&?BZ?`}jC$8=b^T}7WLb$qz5q_JY5Bt;Alev3#K`i048lUg(plnkkQQ( z(Ln4%gJm^-GuXTAkb=1rQlDSok5-oW8|-Nb^q}0I$t)P_@V}FIs(bB1uiP2Ueb1!Q z&J$-_X~Ia2u`at6OKW1hK`?ynie8vv^p(nVi4T>}Z``xuE0s&T*P_(cEN3Yzndy=B zj|Ov*j%(wEyEiPBGSbdYZDsTQBRo7&>Ce{v(L#&nMQ;~93OoyZ4F&|vS4M69C)mp*R@rVN`rF zdu{&?qul8rhc~^Tj^cN=r4-tcm2+Iw6MD9+`x2bePWg@H-JQtKrqjVWBnEaYMS@zL zk%CDA0y|{Ozv#vff8OLHBc0N#6d$Yzjs_$VtDd!D(|xoy>6i-X4#pe3KGVs%Uahe{ z5jMmmzz$qT$Z@iRKEgTLhls=%+}_*a=bQS^kelmEQkd!A^>O&CSkFkTs?$?rXiMUH z9q{>2+!x1=qK>m#Q16Xm^D7PPmel%(QY;5_)pb>Cf@ewVK%E zSGVejRL?VU`#(>2WuSy~_p+^QClA>kH3@V?dQOt=8hMzeXk7ZDiPFVIUD;wZpu8Y5 zqcr6&VrHafs`@%OWn{BKiimEep^g$|W)Ca&H^|r9GcJx#evwiidDA7`{X!ttCqL(X zUaA38nw2kGm-EKV4th^!O1nx(nqLg~0*Cud+Ghr zM4#rwOgDvkUncSON#2oqc;L6GRvpq0pHjVF6p+;SFhOh6Ts)*Hu_k_hqw(c;#CfVM zbODzRD^Xp1wTt^N`KM**>WR?2kbmg^o$+_Kj3R|wi!YO%TH9YhniqBb|5oCv-@i$C zW7Z}-tEuIZUHaAQ&nhYJm)hilF#$fVM?p@GQRy<19$q!U6I^W;5!~!q5wD8xH;CHu zs`jaE3t|kom#hrt+xDil1;PsE5%`}UtS*Xp)4b2~wEwmnFOb{N^tAwnvwBqE6EQ2v z*_!RnUp3B2BYW0D)=U}HXWY=_@i?+;WKsnfXu8+;-qQ+dbNX(Vf{Ds}|Ksz*wCA~w z{uRsxJ{)7))9P)-6GaZ3&IVXgPT^S5hYV%GL?%P|Ie~)ea;5ZQ|9(2=`TT57DK(-v z5z4mubJi&8u62G1-jPme3gZqoj=ZD17Jv5PjhV0FDU&Mp_EkbY1k*iC@o5f@Bv;l| zkY;ss1}wJ%h+PV*H2uZvBmsrF8R2oEe=;$abO%VB%nvh3=8 zGR-=P9eR9M@0D1PN^j7S!3hoXK0vuad(~i%+K6M6#fbZ?S|^IMG|7vjyJ0Dhb|wb$ z9}hqAAi1FA(7ZiZ`2)=ZZvRfHa%B&r-#S0x&^e?7jY zs*gO-kArSsHg-y0>!tNd>cs1NdW4R&_LH^A#SSqVW-;n9dNVbk?@(imgXG6hLmRpl zvNc4(pq~|9Iy^x=A_U$&md*Hy3HPtY%*8kdUt5SaRm87K?^{&C)J`?{rr<+oL zsxm4)W;8w{HM`rOJO6!2@}9P^JcyCUnRvqQ&LW&59ZLP08+Pz|(*FI3q{vg+CjOsK ze1k?dh-FPPJ2ZKm(&hA->#X=iCJXr(e-$}?%zD7rd|5vp90zbPitYP zX*~YR@#e=@wC3O$Ma@OwmZMj{LQykW@`}F6boKDhh*Q1}8ad2^47)o==G#dVvB>_o zH1eO^guhlkr@I&9zcQ2+L;BvvlCq>~ZA4*Pz%&d-fN&R2%vlomMg~)3&NC^) zP9xM$&-+(=IPSJVNJy4ewG#HbAC}Sg9FFzhj?) z#X5c2^)96e!QAr$+#y~)IHNel*k1E`@7B3WU1UlA^YMgrA>H3fxM$e#XRgcB4!ijk z#D(MaYj;;n3hJ~E%&VPni}ya9eiZ-AhjH1Nwx-!f@n)2ahEU3>+^Z1@F@PkOzdwgT zUww7jb2t5l>9u=NI0jOyg5(BU&X}QAhR6VgzoGv0UQ;wusVm>N}M&dFOpTO)tcf2hFyvoQURz%jgL=L~+DI?We^s-<_xr34N25z}|b z=zVO=>zW;UH`L3d-e*FL-Ha=)1_)cq&FaF*CHSNsToFmv7=p?kz6CJI>d`(G1re`T z_#2gTcVW3CtqFyV6qghVeMy>#nuvSk;X(SRjDy~mZpyFJ9^xKT@)I0}#eSG5$0c_Q zpImyg-!ysb!taKhp~pgXJQT0LY(uRQaE*tIE<-w}aHs6m;eRwy;{7JU(%O+TA8T7c zf--%{av8>lpfYiG{mM+Z&U3ku4d3<;nxD(DxfO$KLUPS5Yav(RSGIbv@aj>8f2`_7 z;3KlGRg3#WgvzGk=mWOMitP=L_CvPiyt=0~@+%Zv7GO%s{v2Wy8s&yJROxb5L09C4 zO9e(9->!`}@P-MNBy5qM45m|mP!UR3h}l#MNR+K3q9uH$b4G23U^Z8^la{KzRsOEJ*P~K@@4`D7}CdPg(WXLX+F|-8@QnNC|((fPV{QwW)S;#ZrV^z%@s%bOvhuQ%j|cB+&_;azUo@h z$R?2NpmY+3uW7Cw^}ZiXrG`7P2=OxtSN0UEMvSsQ38Y*1Xt$k+PvgR7UTy28LGmV zM_%lb=ROAFH$PAk`kQpEu)PcM@Rr&bD0IDT-AnM7aQ&Z|&@5_n42(Bb2e|s%Tjd!I ztkNgCG0StjAEih`_{BAPOAjvAYtpbyj?R;*yf%3s{G?LrwB1&n)rIHT~18!KFhQzAG(;0*K8ePkf7v z43$t=b4HK5)f3MNNqrZMgy!^6KTJ9iiPa8?>Jq`%O_dP_bGw@*t0#6iU8^5HyrV>$ zAIJ&$_n&c;>-OfNS9sg;QJnwGK%|NZ!Tzo^eYfkBXeV9ijj-OQj+o%D!Z=-;HHkNb zmmh{A2jv_JG+;DeB03m1A9i*&H4!ON4AqSd(QHx_#-8=3ee7l-Ru()U0|p|7)&yy3!VgyS`WC)7jtoi;c@>3*tH{}ulGsUi)2}gcRF8Ic z^f3=|9twTibzfbINX#6aGc z!^A*^B|<=-K6gfcecaNc-26)1^})x=YO0;|Dn~EnYw%9!_bUgH=tELKyhaFElb$zy zTGu>@e;s}GJEHl`5ZY_lkEkfs`ugDJ#6Inb+fH$DlB}Iq-#ZPFIz>v*w{zAIGE+Pp zXuY?Igv+=B=5oEx@zgd5j(b;gZFcayPZXGul8OYlZWj|5m>ZGYDjJN674E8Z zFehf5E%te%7Odbt-TEkV;^CFpy$=V%TDtm^zhk!Eii3@{(QoY0(`ORf4#t~YgY)g4 zIh&k(tOn4krvfQS9+Ol0hIfFht48I)?s2D>QRw%3F_Nj5I;EtnWCm;b3;jB!kp=U) z>xkyBU$;y@73M+QN@%D~?Y`?H8jAD_`PIhuqDB>#9Y!O(OU*v~sY|v-tC0iMH{cnQzN)Pfw8EN%t%?Bfw zMhd$W>y{vi~pKT0zm+dWN;?o;XD3SscDXEasvpb-(2w^zku zSdMLCrevz}o!~l~W-m(F^r4r`x!w8n`Nyx^xJZt$KIdA6#%=m4>4MvDJzyWSS4fUR%9X>k?OqXxj z@Z_&7T2Dl5(56=sN0QY3b`#9rMzDaH8gQFX=gvocphKE%FV5%2W@B`h^J#WLcI*&Z zX}KtAFVB}9QDidT?^iCU)mp4!{y-ANw8h}@>2VOIa+aI2g)WRn4rR86=s7MCg>Y9L z%8OiU;r3<|$wsHZ8HI}G>Zks`W-gV!gDPjgt%XJT7ZmDEsJZLv5+{m7fTxZR^&54( z->}%@l?SiXy5yl=ZuD{}gSJe&mxo{f+V-Z;N|siommQHAUhE_H-{DkQp0x-?3cm%X zX4t2ygv*lSeqfy#-A1@1R3UowEx=TC26*3&y-4bSdG5@({G6HpdPL^=GInp%LU)Hu zWaF^h{s;rsugb`Lh+g90KHr&?`}bADykzzA6x|<2M#LI5v;}F2UH&c`uP6lHf<;Lv zBXY|vi1Hk+sjlnrw${nX%s4J1d4rtv`(}jk)Rfg&G9k`X=94a#iQJi%w(RAXO-X@l zOzz>oP7lq+HvzqjsfvRdRu%?wR1IVp`*&Sd)q;|0a|FF=6kGgJoRBxGIOA%*#Fg8g z+k*35^BaA{JN<{q;QWA2@z!cvnoX`Q*=%;9zw$uqcaS(^YS~vw17ZoiLc{6*7ak}x z+0yxP@JL7M!2dO?=lsyEH4m4Yt-xAiV>p$8W*W;+m$%=!2dr&57KRe3555bJzZqM) zL#%?-o-{r7_%0rAM>nBxnu+^yFbF4Aqxm3jp(5S=h9Ol^{$MqBTv#|_w983Jr<+jk z@Z+v2D26}nDji7aQx9|Ujp)mo8e)DSZguxkNbI#xfA^}33W=xm_?bMNsYoenNtI3{ z5Suik$iE~YQF}>hzA3RI-KKECY6Ofcro01d5qw=vN(E-xGY#u6G|#MdWm>!5?+h3^ zIs8A`x%O|i&p5o*y4AFG8!f7sOpB>|)m=^8$H8O-V^muem!vc`myBY|Bqc#zf}Ewq z5LvbAT9?%&qli|eLBhmc=CZg%M3Q~guiMW4fS&W7_kGX%ob#UZd@kob&-RpMeoI<3FhMtZL|01A%^{wIRzqYx|DbBfduaEKf5eCg@^lY8iI{9(OvS zB3p^>6~W{QzzH3x8SugvXaAo|IJL>Q(9_3d=|4W^Z|VX`X+?PB`=qA4Y3$^{KeG-T zj-!PCkzo3=4+}~bS_)q@7mG%r#Q}Znla|Z?aJVofTn*Z?2e-d5W9=xwT!<1)b+CSF zkG^Acy4aTMrl3J?DPGmty=lo$P1S3T9wga%sz@!E4%P^E-NU_Y-B!wuQ^en$1+E@~ zLF~UK+c;+RwGm&kk3B~40%qEX%aulf5oCD6hK^pe+C}@8{O-(;YF*7PMN~@5Qs)Gc zN4QBE5tL`&is_B!o~_~ai9V6QWayM2H$@f8BuZM0ePRAqixTn;tm?B@u4~zajj@{1 zu`qQ4(I6sVCr4Pqd*WO%@!El_^H{GaE^?=tY)o0KOEMFYGYROmm3b5jX*UyUM05}r zX=wcgRjJxiZDsI;Juq>i)||j;2d{eB@a9ADtc`PNP+h$UXKdne{z&MZt?C&lcxp`P^@!e*Mcs15qtx{^xeLPa(38Zo?3Ixwy|*H3Ag26 zEbZ$fJ&YYn)*Qm_ykxN)HBEGK2T%}aj~+cTX3i=m1YQamZy?9M9|%N19NRm=x0qr) zcBKueSori*fdSt)C2ry^Kn>Y7>hm*_Q;$a`w9 zpWigvnkBRJJ@U z1#Dp8af`Wo*HU8tOB&qXFOBiLDM9rn+X=G3tEY$=X4&(&A2)tpL?5>ap7zp2aS}MF zF0_w0MA`)jF%}YJepmH#ePLbiiH;vHs78*87nD02Ud5M*tS&K|c=Fl)c97@oaofH_Sa$QX z!f`2&iOmkH%Tl;@GGcoaa-B|{gGN{AlCJYDNrGVWg_W#PmV*etf-$QiDU_;}@B!h@ zec0NX<#5gOe7=N&d64bzsX|L+Y~-49t3G5Lgy|BbGXu9R>U#%2R>8Y`+WAB21?A27 zB;p-+$^A|{>|pg9-e&ssV|}s= zImxVJtp5`5L!+XUhO8*{4qM09!!CW7^QxH+Z-;W=Bj=;4YvxX1gfjO#oARhJffj&S2j5ij1 z6DF|8$=i<87C*OfzfrIyrUfVNt~pPue-HLJfAv)tHb-3_I8D81F85A)p!SrfX70V} zGX}9$5IE2Z|0_}Fl`}*PZ}U%t^H1jM56vgQP6=k`$nI2IEx#2->$Y*ju4>Vur6NdvHq^q6zb^%`??|1|a zp3pjiA~W-jAQMN|BS;0HcQ~uWyxZoTZQjMr|4)X!F1g5H2;-Jm4yfk(o*UT9iRu`Z F@i*ZEpSJ)2 literal 29643 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 literal 22801 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 W-FKg^bGCg43Sv)JKbLh*2~7aN3?W$n diff --git a/test/interpreter_functional/screenshots/baseline/partial_test_3.png b/test/interpreter_functional/screenshots/baseline/partial_test_3.png index ee9182a654d1e8590a02aa23d87abb57144a51a4..7b96f3ec43c7ed6cde24842fb49ffd41f02133be 100644 GIT binary patch literal 7054 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 W-lz9Dga5PXQMT{eb_F{4^}hjykH1|2 literal 7049 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 diff --git a/test/interpreter_functional/screenshots/baseline/tagcloud_all_data.png b/test/interpreter_functional/screenshots/baseline/tagcloud_all_data.png index 03ffc7ac7b1a58b38d4e54eb63ca198a3101021e..a7088de3849a54e103677f10d3d4224921e8feac 100644 GIT binary patch literal 11827 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 XKy)i_PxajewpesD4K>QuZruM5$4YVU literal 15701 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 G$o~LiX#uhT diff --git a/test/interpreter_functional/screenshots/baseline/tagcloud_fontsize.png b/test/interpreter_functional/screenshots/baseline/tagcloud_fontsize.png index 3a7315df405dfe11b32534e49b44700e67e60d9d..8f93ba81ad2adc2d9bfcd4645303607fd962a233 100644 GIT binary patch literal 10314 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 bZTAH~-nel&0XzVK!A_nCJ>K-q=^y_K?U1dR literal 13808 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 aaE4jCpDjfCnrb^JYCK*2T-G@yGywpYYsB;b literal 1920 zcmeAS@N?(olHy`uVBq!ia0y~yV41|gz^KE)1{9egI&&`r1G~GYi(^OyVv%M>FVdQ&MBb@0DHC3!vFvP diff --git a/test/interpreter_functional/screenshots/baseline/tagcloud_metric_data.png b/test/interpreter_functional/screenshots/baseline/tagcloud_metric_data.png index 5cdce6929667362eae577ab636fe34f789debb6d..98890b9687ac95a52ecf4a4299df070eb5e86fa6 100644 GIT binary patch literal 6712 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 mFo1ISpN??m3@2hhkpk)MYIF} literal 9163 zcmeHtYgCix+Gea(r=JH_H>*Vup)Ivk5U8La1jyG?K}28|P()63E2Kyunh23oQtMXQ zjerOVgaFx=Qz;0VATe@?)~!Gi5&;FnDIkOp0tqHS2szF3(A`-xKfX1yX69#qyu9QM zc~19zU)ObC;itcb1nm3|tN%bC5IX~peI15Cyn7pg*mU5(Z3R!tKA?6Y5O%i%zy31f z%BIC9Sz}`S6V%eMrZX8+I=IU{ktrI%StVlHek@T$%lmBy@-u*kbfi`fp z#@eC2!qqe7^ls+a*)QCmTNTU8?_-l^9}zXuLgn=R++11sBH@-)Et)ghZUXatVF z+z=N&-*{zp9KQJHjlaO#FL(Z5-uiAk9K$r!TzNU@R9m;AeU|>sJMhur68Q5!ckY9? zzxurgZ!iD)=UacC#-F+I|EnC9>Tt^w;qV&x3tp2EtT~p9JX}};(L#d=Ez6*#~v*1);%rc)wImIgic~|9b=)T4}4xmR}6ggqs>b%YtP0trwoj;RUt3ju*JMgf%Tm;xa`I2Og+%iw`TWVZ zAPxR!^?^O+(sA!o3)3uvUu8H3N(Bp~%i}$dZ(3@kt!~5;@=mMPFP~$WY5s*1-qoxz z@7fYQ5;;GIO_=%(?Uuga?y36GPtC*&gi2@jCyR@V?5TA}hQF{%YwEjg5zo)}2oa2H z_&sz*8D*!(?pt?RHkBHMQGsb+9NL7KI1TV*Y3seqvH@>>Pes$hRKV;ErI%H&dSce$ zAwIIs+h1EmiYyf7e4>`3D!X!YHLe$CevI(Xw=O!IxymEJlX1HO@UHfQx{y`$*`;*Dr5gomk)6!~7Zb!-$ zijeC59=YuIyLj-`tI9B|44>H8aWK8tqeqXzkJ~j4{4Y;Q{*jT9$M>7 z`jS6h))f3c&aeM|%FFeA7?3VX=x$;uGNZ_Uyh$L`_kVXjE&Bf7j&|u^VG~yua>GTb z3uOxobwNh9qGRmh^voezUj=TSDoT|1i!g)V{ISCMmmBMcV@1A$=;&$#q<6nJJXIKI zPp5E)IjbC;B7-A%TFCEZndI^ep+YIJD-`tZD~+Qf6&*;I&elWIZDMzwLVf3^Ei3lS zXQ5p_ER#$ymOe@k6TM9_XULne!cjtb88-^Gp3&Sj`>3f?Fygu17|-2bUrA-FtN6R7 zSwf>O=g{=?{9xX8mjk^m&TGMS!L+JeTWWDiH-`L^`ExWUl=x{%N=k$E#Yqe1w@>KI z;W*o4oUR<4EFltxmpT6WpmyY&fb(mgJq?TM%FbwJuda_}43D!DREw5gmyb)U4Q8)?PPGdoUd5IL)%9ZQiwZ-rdIaNF(m?)p-67TSx*$yOQE> z#a?<7cl;P~XRPol-4Kzva>h;UBYQcPDSruf z$)53$f@l)gnQEN&gUEW-v~6WM>*nsN(fSFhvSP^)L0mE@&rg28NjqUC==pw=T$&v3 z^NCO9a?h^|>jf-nig|ZEu1AWZwOFTN6y@om&YcU~>_k%`dh+q625;3|MGQ_xW={*+ z)dN96ZmGHyrqb7zyLM@`R&poTwlCi!Sg^qRLt^MYsO7HZxqdqk^5z)* zD9<%*zLKr0vb6NR^4B(T>ZtYV^=piYOnF^`YGzcO8%W~E4l4Ral3U1cM+^HlZOeAu zV`f?LKcczAwkyy#>oHNQ_2lqxQq9iKz4+KuwdBVb3^5;6E!p950yc+R5?!CTH2J0H z@}zL~m0-PJh+jE8TzeNSjLCY1b<5x`ddg-7!#o#Xn&XWv&RnNrY$%AWs=sid(tuid zTQYDgq4&0hT8uUv@)$efNzPd{ew(?9;YlK6D!DbN7>a*&5oyS-(3c|92u?^)69z%H zbPZcma+}*e!N%`04fHp}c*t&G+GM{(#9cWXNKAWzbxj#rh5&;>CN`d^_`+A0qq0j% zP7acMznN3EWxKKBJ@e<1wdt-ZuEET^ti>h`;~>_q9<)TIw*Q}GA#UN4xUWWzJV2gUtG2zuB4=F`kr)-SLzZPAbk^2pj+)ta;G)iwCOxL+n43KZEyDZ^1j3~ z5H7+~E6WE%eGVcQk9<5FgWTUulIo&FuZBao@H%QyQT&Zvz)Wpz&|p9N$ll64{M_02 zKpu!!FT7fWrX=^#%MlO_mq67nh|KB}p7Xc4o}1lFMJj)?CXEe;9}f&bqrJQ=q$*y^ zaj>4&6C6TffnMdIYR?n$tdg6GZ0R|v&K+yFTlsX;W#j?}od9F+B}0&6$G*=8PpO8ozYMalX?y6gqL?$)acrnL#xjWWVQ^9mS zzvQ2hBpU$x2nmq#y_c7&VT|Dk7U;PL*)uE-|Ffqz5&>PRw0;FAn4%L2Pky-WIs4jN zb-~3cw!A>_ED53*h)g~1Wq)@<|IgAecnr3tnXw(aU!atu=QIc6yS(E)=V{b~>Z`34 zCV3vytFOv%@ysr-i>8vW0oO~vt-%;GF(SsYj2Y(;RO*gjD+)jZf*9OL0`NJ0rRM7b zltRA0wx*it269OKCeNi-A}Ngr3x5+qaUpq<{~bl}4E*{miAq%Rt+f*D48ek4x-Te> z&jqoKyE1hh^a-clL16#VITT|TkRw^rPtIV`(;r{M4`@PQemuUq8K)2m`5%u_o(R;{FNPcy%S*3t{P;K!+_INr>t>cVLL-eS6bt#yGT?jFDaUH#>y`81(bOAYISYPdhsT+>1EYWmoP~qCay@RUXwg4fZOG{Tc8#S zfLM@L$=vXO80$a^Jk-y3Fx3(}&Moc_TeiCawAaexOC_*oC4gU~svuHjdwn`zc`$luIYps8PO)nyiCjnsOKuW z)#FNQT4*+g-Wrqk?5K9uqst2M?JFQ3v45c>FPgHiD9!2^dP%7pfV_qsd%QlyOMQ#j zczVyeL07*~(Q*2(wqg%`?$k|`w~Lax-}78k(|bRpVavmg$1Mg0Sl@=zfpGQadxQ$$ ztS_Dx)_~$MG_T#1uw6Yf)%7dxiBQ-l#-G2-uK7T*obv$0Rm(!pmsS!sluTm_ z+Zis(P3TSIR^x4gc<<-hR^0W+7F`;}(rBsW6;Sj1y72aD;dua^stdz$E2BHCk9_2) z8xu|f+opU-O#scN5^fhY$DO0GsRnR4Vr73t-F5k}bZbY^Le^D^ak zX{)QN0QiPJL|iF;jmzsBP`wNft(PdW5@+D`3dYA)H#Y%Mlb?Y}1r*V$mlY&6z}3`e zD@R4y>R=`VvKa-gf6Mj{69E-K@VBWj9CL`ypZKKqP?+f3YgfTqLqRrG7y4F_0a_F# z9kOb`Je=B^Bnu%p#OvnY*Uk?YK%hMt8DHAi+~vHHxIfuj)4FYFrIa9-cGnhn!bEX~ zR`LNn+D3qeDFaM&^cgQNFZ^#m)z7qzDtqLjwBbs;ND?UlD8e9`hz;3q&go|U;n)%k zhE6L^J;qyG?5?XV@i$s-H+FpC6rFlMsw#6{);g9Wya}(45zup;fAjaZqQzKc4-}!` z^=tr|UQC%T@Nt0D1#;qT>rE@qPskt=g$qpd8RvHWd}~amh@9~NhK}db(!<@lbL0y$ zK){;X_C`l?w=KPt>mFSgBCJYjSe6fa0O$<_sr2$7X6|{C<)gQ^AU`|CW$Yu{)3Y6% zs{On6IaLQ>H{|cH%L^jP@IZAyVaLdv6RQVGOo>*C9N>^RO_(V+VdArYfI}H_+mm|a z3o`-nQ!)%j7&zd2PacC(uNPrv`vYu+sB?p?f)k*u6Q1t$I2J^Bv>BnFhJf?cwXG>- z6vzNfX_b=Km~ZX*$zo@gv{YJINp({SH#2Rx;{-7c2vq*hM}-pDyAZS9W<-h1Lj}Sf z*3DxHtA2$wuyUL3Kew_xjQd*APC7Nq&p0qXJ`O@Ho15Fv39s+vs|$An5gL;tdB)Z$ zveYm93TG+%Sx`;6878QnreO(n1qP|L{J0LDdLRF(zkmrJ-XnWm>)P^HKKOy8T+2P!LoLAPAcy z@6k6B4RUc^g1XNm*km*esJb*p^y-*8C*whpT|iA_%VGQgfkO^m)bx)L#GBbl<2L+s zaB;Y&5h)fHAVYdSd+Hv-gS!T0A1VQzeS%h1>UMehiRj97{s3;~{oKUmxjrTXq()nt zO+)f@er`=Q{?e?syGKih>UiezToI5RCqU?%cKLbO+@^Hf&}seAsa4#jD5g4-ND^Id zNai_t1o5yg>`!uS^rBW+dA`Qp&EXp<87MZKf^GqXTTcKUzc=Ba2nI3k;g;^0>81rL zx+<7g9P0Z4Vsd5izOFfH?{UOr@GqSNS6?K7e9I4|Bf zP}Jw1lYGV|bFB!&=zW1DSZ zPlwIhcJ-W%Kl1rG*0|STU9hk*e>|Pmdf?1*j6*c`u$OuI?ox==5$PkBzJjNIT>9pj zr&foaslb!S3WcHrwCEX!2oq_c&0UFMpvxM@tvo0WqaHaMb7RNE=OF#jFMHl^fYbzN zB))Cp0uL5`#oMP${Dh0k34D^9)S&v!`ts8I+9&%>l_tHA0E7?G5vabdq^L3NS8ZcB zb*o;b&r_9hyy-M7`*6VCoR}`{U31u5GDLOQV4j~D>UOZZWw{gU^?}HExBY0t9Gd;+ zAISsX9SR$!M5j`Y1Kbh{JyDP_3Q4%sQ>DQkd&v|Svkf$!Tfn;lSd5Bw7iSOdu<`z! ztR`lh4M9$K11+-*T5KhL*~94Qq$G(I4L3!JjS;Y6B9t0^p-skFlWa+{?35$|EPk@J z0cM(Wr~X&qJ@`Q9KqZ{-uMRk4ld=5vK=dl>532Ce-@Ar?{eA!}TJ@7o-y%RGz@&*- z9Y+hBI7HL3QtaX}acG}r9OVc?6B;e7ifWx>NBFK?)zksIC&5FEOHS>Q3^gsBvmHm0 z3m4ka&F#kLFnA%JD}3noB#F}br+^+o);I5TfBlirM-v0s$d!pkRX{GNtaPc7jJJ2% z$(!3lV9%gms8GAf)@iQbVlZ+vB1dv#OH~O_eh%cf)2xkTKWXWkeOmEQt$-83ic;&< zd!b5+lnSzj$9Q^vMt~DlSr6wj_d$QrCEhiRcLK9bn z(jJXV8CxBl)!CDf`Tlm~r)J0_xqB}~%nC_fwqtVh-x=>jf;eZDzb@AZ@15Nv?M8We zkWH_1T6XO9`V<;~T+lGMX0P4bVd;7VcFL^KPB$nW;pGI(M^RG!Pf>*NG~A1;Iw@LO zK@S`jm%xj>*e;CI24qhflt6UGmvhObPLoLqV^sF(Q@}) zgfX8yVAtq+Hg^%f-e8|dyBknHfN^&N>MeUc+?EHcmu$BS`6)y>40B};sy;w3aIM>j ztKpvdyls+Ku)*8V-63nM9?&Uk*s=Q_k(jwE6D3IZ4ubh%tD`ddqzjYkE6QNtxWLAA z0wJkAgc~B6KC(5v&V>yIWLR(p*S48)d+lvX2@8yH2dK26YvlU3wR^1?kMt7-zVkf` ztIe@tmTsU{U}G^w+ra}PMv2g7uU(Yla~i;?XxI|9cM=MY=qEcUd&O;jXF%4_xsy+> zLAR|H!x=AYy=e&`5w=)M!8d9Po?P1|*{mdmc4_llW?tH6dvs>rrBa})1znO+jE@%? zbVbk;8KkeD-lhBP%Z3j8aHYC(np4D59y&+{!aF2^Md-9cdFqxUpo0`B_HoWqu3TM~ zkJ>xI5iBZ9#XC*|&HG<(3y^V!Pi;V;1lSQ$zAX_2!JTKpSz<$fsi_H;XYF#2JV_30 zwkn@doGQ>n7LpW$upS{e7HHQ?)l;3QTd$I;^S4SW=mvsshi04G#Q>IK;l7>UM~c8a%~Vz3$-!Pu%4J;Pcq%ysMfI@X`gIc-XW)+{AuvB?Is`e;Sz6aoMuk z<;6k!#&C5DsjnPZOEx~4=hM!#EnuVCFj4EvQ7(;)zdiehHyCm2j@`GytfI(LqbhqS z8av+9&qSYsjdb1JyG~G6#jk)Cn=y1|5jb|ddB*wPyo{OtZU&%9k#-RqME8}~s@{g$ zlzA5R^?{61>3Gik`Jw)PYT8hgy-!w~WTivJb%ekLXgmlQ{mD^qiNuVfa5Jy&w6`OJ zC>>?+uWJthYrGA7kqR3f_)S5a@J#Xp^xX#|Y7MYifMDyWf;)M`O+U7^wQX!V8lnd34p2vfz&FeA0cJMT^xCYgO=8sVpLmtfxiZu@-_|PPM;3Uw zgF1r*EkFkIE4pW)gAfk8WvS`!AVhTq|DIBvIWjUbFf>GYRSWVC46qZiv=07f(o}Q;H?{_x)c^*k7w diff --git a/test/interpreter_functional/screenshots/baseline/tagcloud_options.png b/test/interpreter_functional/screenshots/baseline/tagcloud_options.png index 394b5585097a24da5e3c10ed2c1b50bf8a9cc1cb..479280f598aef7d02121f8874c21c223c9f642dc 100644 GIT binary patch literal 15100 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 aHE&iz8^0!7DlG?zEGmi`4~yiVy!s!XBR4+) literal 18998 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 V8LWggH9(gQQeyINi$(N5{y#)d+qwV% diff --git a/test/interpreter_functional/snapshots/baseline/combined_test2.json b/test/interpreter_functional/snapshots/baseline/combined_test2.json index 98c7844e41f19b..84203617ff8535 100644 --- a/test/interpreter_functional/snapshots/baseline/combined_test2.json +++ b/test/interpreter_functional/snapshots/baseline/combined_test2.json @@ -1 +1 @@ -{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"} \ No newline at end of file +{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/combined_test3.json b/test/interpreter_functional/snapshots/baseline/combined_test3.json index 310377eadd165a..af9fe198d88ea5 100644 --- a/test/interpreter_functional/snapshots/baseline/combined_test3.json +++ b/test/interpreter_functional/snapshots/baseline/combined_test3.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/final_output_test.json b/test/interpreter_functional/snapshots/baseline/final_output_test.json index 310377eadd165a..af9fe198d88ea5 100644 --- a/test/interpreter_functional/snapshots/baseline/final_output_test.json +++ b/test/interpreter_functional/snapshots/baseline/final_output_test.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/metric_all_data.json b/test/interpreter_functional/snapshots/baseline/metric_all_data.json index 26f111e9edcf92..9b0122c1574812 100644 --- a/test/interpreter_functional/snapshots/baseline/metric_all_data.json +++ b/test/interpreter_functional/snapshots/baseline/metric_all_data.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":2,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"},{"id":"col-2-1","name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":2,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"},{"id":"col-2-1","meta":{"aggConfigParams":{"field":"bytes"},"indexPatternId":"logstash-*","type":"max"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/metric_multi_metric_data.json b/test/interpreter_functional/snapshots/baseline/metric_multi_metric_data.json index 7f64f978451915..2d6e756a7f0a3a 100644 --- a/test/interpreter_functional/snapshots/baseline/metric_multi_metric_data.json +++ b/test/interpreter_functional/snapshots/baseline/metric_multi_metric_data.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"},{"id":"col-2-1","name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"},{"id":"col-2-1","meta":{"aggConfigParams":{"field":"bytes"},"indexPatternId":"logstash-*","type":"max"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/metric_percentage_mode.json b/test/interpreter_functional/snapshots/baseline/metric_percentage_mode.json index e171a65be8bab5..37c6885d76cb08 100644 --- a/test/interpreter_functional/snapshots/baseline/metric_percentage_mode.json +++ b/test/interpreter_functional/snapshots/baseline/metric_percentage_mode.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":1000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":true,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"},{"id":"col-2-1","name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":1000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":true,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"},{"id":"col-2-1","meta":{"aggConfigParams":{"field":"bytes"},"indexPatternId":"logstash-*","type":"max"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/metric_single_metric_data.json b/test/interpreter_functional/snapshots/baseline/metric_single_metric_data.json index ed8b0b258fd90f..60a0e450906a27 100644 --- a/test/interpreter_functional/snapshots/baseline/metric_single_metric_data.json +++ b/test/interpreter_functional/snapshots/baseline/metric_single_metric_data.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"},{"id":"col-2-1","name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"},{"id":"col-2-1","meta":{"aggConfigParams":{"field":"bytes"},"indexPatternId":"logstash-*","type":"max"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/partial_test_1.json b/test/interpreter_functional/snapshots/baseline/partial_test_1.json index 8a349aa5df0600..6b2f93b47c0b28 100644 --- a/test/interpreter_functional/snapshots/baseline/partial_test_1.json +++ b/test/interpreter_functional/snapshots/baseline/partial_test_1.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/partial_test_2.json b/test/interpreter_functional/snapshots/baseline/partial_test_2.json index 310377eadd165a..af9fe198d88ea5 100644 --- a/test/interpreter_functional/snapshots/baseline/partial_test_2.json +++ b/test/interpreter_functional/snapshots/baseline/partial_test_2.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/partial_test_3.json b/test/interpreter_functional/snapshots/baseline/partial_test_3.json index c1e429508c37ff..4241d6f208bfd4 100644 --- a/test/interpreter_functional/snapshots/baseline/partial_test_3.json +++ b/test/interpreter_functional/snapshots/baseline/partial_test_3.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":0},"metric":{"accessor":1,"format":{"id":"number"}}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"region_map"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":0},"metric":{"accessor":1,"format":{"id":"number"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"region_map"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/step_output_test2.json b/test/interpreter_functional/snapshots/baseline/step_output_test2.json index 98c7844e41f19b..84203617ff8535 100644 --- a/test/interpreter_functional/snapshots/baseline/step_output_test2.json +++ b/test/interpreter_functional/snapshots/baseline/step_output_test2.json @@ -1 +1 @@ -{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"} \ No newline at end of file +{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/step_output_test3.json b/test/interpreter_functional/snapshots/baseline/step_output_test3.json index 310377eadd165a..af9fe198d88ea5 100644 --- a/test/interpreter_functional/snapshots/baseline/step_output_test3.json +++ b/test/interpreter_functional/snapshots/baseline/step_output_test3.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_all_data.json b/test/interpreter_functional/snapshots/baseline/tagcloud_all_data.json index 1325c7fbed03ea..ae1e817424cb10 100644 --- a/test/interpreter_functional/snapshots/baseline/tagcloud_all_data.json +++ b/test/interpreter_functional/snapshots/baseline/tagcloud_all_data.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_fontsize.json b/test/interpreter_functional/snapshots/baseline/tagcloud_fontsize.json index 2b063b518665af..c0da4794728807 100644 --- a/test/interpreter_functional/snapshots/baseline/tagcloud_fontsize.json +++ b/test/interpreter_functional/snapshots/baseline/tagcloud_fontsize.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":40,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":20,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":40,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":20,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_metric_data.json b/test/interpreter_functional/snapshots/baseline/tagcloud_metric_data.json index 6152fd406961fc..c5fbcd63b06854 100644 --- a/test/interpreter_functional/snapshots/baseline/tagcloud_metric_data.json +++ b/test/interpreter_functional/snapshots/baseline/tagcloud_metric_data.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_options.json b/test/interpreter_functional/snapshots/baseline/tagcloud_options.json index e4c6b09a264ddb..b67b0744494034 100644 --- a/test/interpreter_functional/snapshots/baseline/tagcloud_options.json +++ b/test/interpreter_functional/snapshots/baseline/tagcloud_options.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"multiple","scale":"log","showLabel":true},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"multiple","scale":"log","showLabel":true},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/combined_test2.json b/test/interpreter_functional/snapshots/session/combined_test2.json index 98c7844e41f19b..84203617ff8535 100644 --- a/test/interpreter_functional/snapshots/session/combined_test2.json +++ b/test/interpreter_functional/snapshots/session/combined_test2.json @@ -1 +1 @@ -{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"} \ No newline at end of file +{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/combined_test3.json b/test/interpreter_functional/snapshots/session/combined_test3.json index 310377eadd165a..af9fe198d88ea5 100644 --- a/test/interpreter_functional/snapshots/session/combined_test3.json +++ b/test/interpreter_functional/snapshots/session/combined_test3.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/final_output_test.json b/test/interpreter_functional/snapshots/session/final_output_test.json index 310377eadd165a..af9fe198d88ea5 100644 --- a/test/interpreter_functional/snapshots/session/final_output_test.json +++ b/test/interpreter_functional/snapshots/session/final_output_test.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/metric_all_data.json b/test/interpreter_functional/snapshots/session/metric_all_data.json index 26f111e9edcf92..9b0122c1574812 100644 --- a/test/interpreter_functional/snapshots/session/metric_all_data.json +++ b/test/interpreter_functional/snapshots/session/metric_all_data.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":2,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"},{"id":"col-2-1","name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":2,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"},{"id":"col-2-1","meta":{"aggConfigParams":{"field":"bytes"},"indexPatternId":"logstash-*","type":"max"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/metric_multi_metric_data.json b/test/interpreter_functional/snapshots/session/metric_multi_metric_data.json index 7f64f978451915..2d6e756a7f0a3a 100644 --- a/test/interpreter_functional/snapshots/session/metric_multi_metric_data.json +++ b/test/interpreter_functional/snapshots/session/metric_multi_metric_data.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"},{"id":"col-2-1","name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"},{"id":"col-2-1","meta":{"aggConfigParams":{"field":"bytes"},"indexPatternId":"logstash-*","type":"max"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/metric_percentage_mode.json b/test/interpreter_functional/snapshots/session/metric_percentage_mode.json index e171a65be8bab5..37c6885d76cb08 100644 --- a/test/interpreter_functional/snapshots/session/metric_percentage_mode.json +++ b/test/interpreter_functional/snapshots/session/metric_percentage_mode.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":1000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":true,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"},{"id":"col-2-1","name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":1000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":true,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"},{"id":"col-2-1","meta":{"aggConfigParams":{"field":"bytes"},"indexPatternId":"logstash-*","type":"max"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/metric_single_metric_data.json b/test/interpreter_functional/snapshots/session/metric_single_metric_data.json index ed8b0b258fd90f..60a0e450906a27 100644 --- a/test/interpreter_functional/snapshots/session/metric_single_metric_data.json +++ b/test/interpreter_functional/snapshots/session/metric_single_metric_data.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"},{"id":"col-2-1","name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"},{"id":"col-2-1","meta":{"aggConfigParams":{"field":"bytes"},"indexPatternId":"logstash-*","type":"max"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/partial_test_1.json b/test/interpreter_functional/snapshots/session/partial_test_1.json index 8a349aa5df0600..6b2f93b47c0b28 100644 --- a/test/interpreter_functional/snapshots/session/partial_test_1.json +++ b/test/interpreter_functional/snapshots/session/partial_test_1.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/partial_test_2.json b/test/interpreter_functional/snapshots/session/partial_test_2.json index 310377eadd165a..af9fe198d88ea5 100644 --- a/test/interpreter_functional/snapshots/session/partial_test_2.json +++ b/test/interpreter_functional/snapshots/session/partial_test_2.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/partial_test_3.json b/test/interpreter_functional/snapshots/session/partial_test_3.json index c1e429508c37ff..4241d6f208bfd4 100644 --- a/test/interpreter_functional/snapshots/session/partial_test_3.json +++ b/test/interpreter_functional/snapshots/session/partial_test_3.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":0},"metric":{"accessor":1,"format":{"id":"number"}}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"region_map"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":0},"metric":{"accessor":1,"format":{"id":"number"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"region_map"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/step_output_test2.json b/test/interpreter_functional/snapshots/session/step_output_test2.json index 98c7844e41f19b..84203617ff8535 100644 --- a/test/interpreter_functional/snapshots/session/step_output_test2.json +++ b/test/interpreter_functional/snapshots/session/step_output_test2.json @@ -1 +1 @@ -{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"} \ No newline at end of file +{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/step_output_test3.json b/test/interpreter_functional/snapshots/session/step_output_test3.json index 310377eadd165a..af9fe198d88ea5 100644 --- a/test/interpreter_functional/snapshots/session/step_output_test3.json +++ b/test/interpreter_functional/snapshots/session/step_output_test3.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/tagcloud_all_data.json b/test/interpreter_functional/snapshots/session/tagcloud_all_data.json index 1325c7fbed03ea..ae1e817424cb10 100644 --- a/test/interpreter_functional/snapshots/session/tagcloud_all_data.json +++ b/test/interpreter_functional/snapshots/session/tagcloud_all_data.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/tagcloud_fontsize.json b/test/interpreter_functional/snapshots/session/tagcloud_fontsize.json index 2b063b518665af..c0da4794728807 100644 --- a/test/interpreter_functional/snapshots/session/tagcloud_fontsize.json +++ b/test/interpreter_functional/snapshots/session/tagcloud_fontsize.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":40,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":20,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":40,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":20,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/tagcloud_metric_data.json b/test/interpreter_functional/snapshots/session/tagcloud_metric_data.json index 6152fd406961fc..c5fbcd63b06854 100644 --- a/test/interpreter_functional/snapshots/session/tagcloud_metric_data.json +++ b/test/interpreter_functional/snapshots/session/tagcloud_metric_data.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/tagcloud_options.json b/test/interpreter_functional/snapshots/session/tagcloud_options.json index e4c6b09a264ddb..b67b0744494034 100644 --- a/test/interpreter_functional/snapshots/session/tagcloud_options.json +++ b/test/interpreter_functional/snapshots/session/tagcloud_options.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"multiple","scale":"log","showLabel":true},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"multiple","scale":"log","showLabel":true},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file From c76519e15c4ca10c9e2c7fc5b0dcb8216d7661b0 Mon Sep 17 00:00:00 2001 From: Victor Martinez Date: Mon, 3 Feb 2020 14:02:28 +0000 Subject: [PATCH 27/33] enable darwin for the node installation in the CI (#51705) * enable darwin for the node installation in the CI * refactor: avoid hardcode strings and customise based on the OS flavour * fix classifier Co-authored-by: Elastic Machine --- src/dev/ci_setup/setup_env.sh | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/dev/ci_setup/setup_env.sh b/src/dev/ci_setup/setup_env.sh index 5217fdf002be9c..823c70e80fe7cd 100644 --- a/src/dev/ci_setup/setup_env.sh +++ b/src/dev/ci_setup/setup_env.sh @@ -56,23 +56,23 @@ export KIBANA_PKG_BRANCH="$kbnBranch" ### ### download node ### +nodeVersion="$(cat "$dir/.node-version")" +nodeDir="$cacheDir/node/$nodeVersion" +nodeBin="$nodeDir/bin" +classifier="x64.tar.gz" + UNAME=$(uname) OS="linux" if [[ "$UNAME" = *"MINGW64_NT"* ]]; then OS="win" + nodeBin="$HOME/node" + classifier="x64.zip" +elif [[ "$UNAME" == "Darwin" ]]; then + OS="darwin" fi echo " -- Running on OS: $OS" -nodeVersion="$(cat "$dir/.node-version")" -nodeDir="$cacheDir/node/$nodeVersion" - -if [[ "$OS" == "win" ]]; then - nodeBin="$HOME/node" - nodeUrl="https://us-central1-elastic-kibana-184716.cloudfunctions.net/kibana-ci-proxy-cache/dist/v$nodeVersion/node-v$nodeVersion-win-x64.zip" -else - nodeBin="$nodeDir/bin" - nodeUrl="https://us-central1-elastic-kibana-184716.cloudfunctions.net/kibana-ci-proxy-cache/dist/v$nodeVersion/node-v$nodeVersion-linux-x64.tar.gz" -fi +nodeUrl="https://us-central1-elastic-kibana-184716.cloudfunctions.net/kibana-ci-proxy-cache/dist/v$nodeVersion/node-v$nodeVersion-${OS}-${classifier}" if [[ "$installNode" == "true" ]]; then echo " -- node: version=v${nodeVersion} dir=$nodeDir" From b64f0a76fd4eda536f6fc0cda891a45620249e9c Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Mon, 3 Feb 2020 15:33:57 +0100 Subject: [PATCH 28/33] [ML] Setup apiDocs to generate routes docs (#56006) * [ML] setup typedoc to generate routes docs * [ML] remove typedoc packages * [ML] apiDoc * [ML] update optional params * [ML] address pr comments * [ML] change names * [ML] change description for GetDataFrameAnalyticsMessages * [ML] add custom order --- x-pack/legacy/plugins/ml/.gitignore | 1 + .../legacy/plugins/ml/server/routes/README.md | 16 +++ .../plugins/ml/server/routes/apidoc.json | 21 ++++ .../ml/server/routes/data_frame_analytics.ts | 107 ++++++++++++++++++ 4 files changed, 145 insertions(+) create mode 100644 x-pack/legacy/plugins/ml/.gitignore create mode 100644 x-pack/legacy/plugins/ml/server/routes/README.md create mode 100644 x-pack/legacy/plugins/ml/server/routes/apidoc.json diff --git a/x-pack/legacy/plugins/ml/.gitignore b/x-pack/legacy/plugins/ml/.gitignore new file mode 100644 index 00000000000000..708c5b199467b9 --- /dev/null +++ b/x-pack/legacy/plugins/ml/.gitignore @@ -0,0 +1 @@ +routes_doc diff --git a/x-pack/legacy/plugins/ml/server/routes/README.md b/x-pack/legacy/plugins/ml/server/routes/README.md new file mode 100644 index 00000000000000..1d08335af3d2e9 --- /dev/null +++ b/x-pack/legacy/plugins/ml/server/routes/README.md @@ -0,0 +1,16 @@ +# ML Kibana API routes + +This folder contains ML API routes in Kibana. + +Each route handler requires [apiDoc](https://github.com/apidoc/apidoc) annotations in order +to generate documentation. +The [apidoc-markdown](https://github.com/rigwild/apidoc-markdown) package is also required in order to generate the markdown. + +For now the process is pretty manual. You need to make sure the packages mentioned above are installed globally +to execute the following command from the directory in which this README file is located. +``` +apidoc -i . -o ../routes_doc && apidoc-markdown -p ../routes_doc -o ../routes_doc/ML_API.md +``` + +It will create a new directory `routes_doc` (next to the `routes` folder) which contains the documentation in HTML format +as well as `ML_API.md` file. \ No newline at end of file diff --git a/x-pack/legacy/plugins/ml/server/routes/apidoc.json b/x-pack/legacy/plugins/ml/server/routes/apidoc.json new file mode 100644 index 00000000000000..8292e946cd344d --- /dev/null +++ b/x-pack/legacy/plugins/ml/server/routes/apidoc.json @@ -0,0 +1,21 @@ +{ + "name": "ml_kibana_api", + "version": "0.1.0", + "description": "ML Kibana API", + "title": "ML Kibana API", + "url" : "/api/ml/", + "order": [ + "DataFrameAnalytics", + "GetDataFrameAnalytics", + "GetDataFrameAnalyticsById", + "GetDataFrameAnalyticsStats", + "GetDataFrameAnalyticsStatsById", + "UpdateDataFrameAnalytics", + "EvaluateDataFrameAnalytics", + "ExplainDataFrameAnalytics", + "DeleteDataFrameAnalytics", + "StartDataFrameAnalyticsJob", + "StopsDataFrameAnalyticsJob", + "GetDataFrameAnalyticsMessages" + ] +} diff --git a/x-pack/legacy/plugins/ml/server/routes/data_frame_analytics.ts b/x-pack/legacy/plugins/ml/server/routes/data_frame_analytics.ts index 7b855e5f87cbf6..67fa2fba46f1a9 100644 --- a/x-pack/legacy/plugins/ml/server/routes/data_frame_analytics.ts +++ b/x-pack/legacy/plugins/ml/server/routes/data_frame_analytics.ts @@ -15,7 +15,20 @@ import { dataAnalyticsExplainSchema, } from '../new_platform/data_analytics_schema'; +/** + * Routes for the data frame analytics + */ export function dataFrameAnalyticsRoutes({ xpackMainPlugin, router }: RouteInitialization) { + /** + * @apiGroup DataFrameAnalytics + * + * @api {get} /api/ml/data_frame/analytics Get analytics data + * @apiName GetDataFrameAnalytics + * @apiDescription Returns the list of data frame analytics jobs. + * + * @apiSuccess {Number} count + * @apiSuccess {Object[]} data_frame_analytics + */ router.get( { path: '/api/ml/data_frame/analytics', @@ -35,6 +48,15 @@ export function dataFrameAnalyticsRoutes({ xpackMainPlugin, router }: RouteIniti }) ); + /** + * @apiGroup DataFrameAnalytics + * + * @api {get} /api/ml/data_frame/analytics/:analyticsId Get analytics data by id + * @apiName GetDataFrameAnalyticsById + * @apiDescription Returns the data frame analytics job. + * + * @apiParam {String} analyticsId Analytics ID. + */ router.get( { path: '/api/ml/data_frame/analytics/{analyticsId}', @@ -57,6 +79,13 @@ export function dataFrameAnalyticsRoutes({ xpackMainPlugin, router }: RouteIniti }) ); + /** + * @apiGroup DataFrameAnalytics + * + * @api {get} /api/ml/data_frame/analytics/_stats Get analytics stats + * @apiName GetDataFrameAnalyticsStats + * @apiDescription Returns data frame analytics jobs statistics. + */ router.get( { path: '/api/ml/data_frame/analytics/_stats', @@ -76,6 +105,15 @@ export function dataFrameAnalyticsRoutes({ xpackMainPlugin, router }: RouteIniti }) ); + /** + * @apiGroup DataFrameAnalytics + * + * @api {get} /api/ml/data_frame/analytics/:analyticsId/_stats Get stats for requested analytics job + * @apiName GetDataFrameAnalyticsStatsById + * @apiDescription Returns data frame analytics job statistics. + * + * @apiParam {String} analyticsId Analytics ID. + */ router.get( { path: '/api/ml/data_frame/analytics/{analyticsId}/_stats', @@ -101,6 +139,16 @@ export function dataFrameAnalyticsRoutes({ xpackMainPlugin, router }: RouteIniti }) ); + /** + * @apiGroup DataFrameAnalytics + * + * @api {put} /api/ml/data_frame/analytics/:analyticsId Instantiate a data frame analytics job + * @apiName UpdateDataFrameAnalytics + * @apiDescription This API creates a data frame analytics job that performs an analysis + * on the source index and stores the outcome in a destination index. + * + * @apiParam {String} analyticsId Analytics ID. + */ router.put( { path: '/api/ml/data_frame/analytics/{analyticsId}', @@ -130,6 +178,13 @@ export function dataFrameAnalyticsRoutes({ xpackMainPlugin, router }: RouteIniti }) ); + /** + * @apiGroup DataFrameAnalytics + * + * @api {post} /api/ml/data_frame/_evaluate Evaluate the data frame analytics for an annotated index + * @apiName EvaluateDataFrameAnalytics + * @apiDescription Evaluates the data frame analytics for an annotated index. + */ router.post( { path: '/api/ml/data_frame/_evaluate', @@ -154,6 +209,22 @@ export function dataFrameAnalyticsRoutes({ xpackMainPlugin, router }: RouteIniti }) ); + /** + * @apiGroup DataFrameAnalytics + * + * @api {post} /api/ml/data_frame/_explain Explain a data frame analytics config + * @apiName ExplainDataFrameAnalytics + * @apiDescription This API provides explanations for a data frame analytics config + * that either exists already or one that has not been created yet. + * + * @apiParam {String} [description] + * @apiParam {Object} [dest] + * @apiParam {Object} source + * @apiParam {String} source.index + * @apiParam {Object} analysis + * @apiParam {Object} [analyzed_fields] + * @apiParam {String} [model_memory_limit] + */ router.post( { path: '/api/ml/data_frame/analytics/_explain', @@ -178,6 +249,15 @@ export function dataFrameAnalyticsRoutes({ xpackMainPlugin, router }: RouteIniti }) ); + /** + * @apiGroup DataFrameAnalytics + * + * @api {delete} /api/ml/data_frame/analytics/:analyticsId Delete specified analytics job + * @apiName DeleteDataFrameAnalytics + * @apiDescription Deletes specified data frame analytics job. + * + * @apiParam {String} analyticsId Analytics ID. + */ router.delete( { path: '/api/ml/data_frame/analytics/{analyticsId}', @@ -205,6 +285,15 @@ export function dataFrameAnalyticsRoutes({ xpackMainPlugin, router }: RouteIniti }) ); + /** + * @apiGroup DataFrameAnalytics + * + * @api {post} /api/ml/data_frame/analytics/:analyticsId/_start Start specified analytics job + * @apiName StartDataFrameAnalyticsJob + * @apiDescription Starts a data frame analytics job. + * + * @apiParam {String} analyticsId Analytics ID. + */ router.post( { path: '/api/ml/data_frame/analytics/{analyticsId}/_start', @@ -229,6 +318,15 @@ export function dataFrameAnalyticsRoutes({ xpackMainPlugin, router }: RouteIniti }) ); + /** + * @apiGroup DataFrameAnalytics + * + * @api {post} /api/ml/data_frame/analytics/:analyticsId/_stop Stop specified analytics job + * @apiName StopsDataFrameAnalyticsJob + * @apiDescription Stops a data frame analytics job. + * + * @apiParam {String} analyticsId Analytics ID. + */ router.post( { path: '/api/ml/data_frame/analytics/{analyticsId}/_stop', @@ -263,6 +361,15 @@ export function dataFrameAnalyticsRoutes({ xpackMainPlugin, router }: RouteIniti }) ); + /** + * @apiGroup DataFrameAnalytics + * + * @api {get} /api/ml/data_frame/analytics/:analyticsId/messages Get analytics job messages + * @apiName GetDataFrameAnalyticsMessages + * @apiDescription Returns the list of audit messages for data frame analytics jobs. + * + * @apiParam {String} analyticsId Analytics ID. + */ router.get( { path: '/api/ml/data_frame/analytics/{analyticsId}/messages', From 7dc8c6f9ba15d4566147dbac144b32becdf2adee Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Mon, 3 Feb 2020 07:51:53 -0700 Subject: [PATCH 29/33] [Uptime] Fix flaky functional test for #54541 (#56449) * Add timeout block to race-prone functional test code. * Add timeout for pagination click functions. Co-authored-by: Elastic Machine --- x-pack/test/functional/apps/uptime/overview.ts | 12 ++++++++---- x-pack/test/functional/services/uptime.ts | 4 ++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/x-pack/test/functional/apps/uptime/overview.ts b/x-pack/test/functional/apps/uptime/overview.ts index 73b91a61196bfa..9a879032fadc14 100644 --- a/x-pack/test/functional/apps/uptime/overview.ts +++ b/x-pack/test/functional/apps/uptime/overview.ts @@ -53,8 +53,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('pagination is cleared when filter criteria changes', async () => { await pageObjects.uptime.goToUptimePageAndSetDateRange(DEFAULT_DATE_START, DEFAULT_DATE_END); await pageObjects.uptime.changePage('next'); - // there should now be pagination data in the URL - await pageObjects.uptime.pageUrlContains('pagination'); await pageObjects.uptime.pageHasExpectedIds([ '0010-down', '0011-up', @@ -67,9 +65,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { '0018-up', '0019-up', ]); + await retry.tryForTime(12000, async () => { + // there should now be pagination data in the URL + await pageObjects.uptime.pageUrlContains('pagination'); + }); await pageObjects.uptime.setStatusFilter('up'); - // ensure that pagination is removed from the URL - await pageObjects.uptime.pageUrlContains('pagination', false); await pageObjects.uptime.pageHasExpectedIds([ '0000-intermittent', '0001-up', @@ -82,6 +82,10 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { '0008-up', '0009-up', ]); + await retry.tryForTime(12000, async () => { + // ensure that pagination is removed from the URL + await pageObjects.uptime.pageUrlContains('pagination', false); + }); }); describe('snapshot counts', () => { diff --git a/x-pack/test/functional/services/uptime.ts b/x-pack/test/functional/services/uptime.ts index ed39f28aabbfad..1d8e0c97b99c4d 100644 --- a/x-pack/test/functional/services/uptime.ts +++ b/x-pack/test/functional/services/uptime.ts @@ -38,10 +38,10 @@ export function UptimeProvider({ getService }: FtrProviderContext) { await browser.pressKeys(browser.keys.ENTER); }, async goToNextPage() { - await testSubjects.click('xpack.uptime.monitorList.nextButton'); + await testSubjects.click('xpack.uptime.monitorList.nextButton', 5000); }, async goToPreviousPage() { - await testSubjects.click('xpack.uptime.monitorList.prevButton'); + await testSubjects.click('xpack.uptime.monitorList.prevButton', 5000); }, async setStatusFilterUp() { await testSubjects.click('xpack.uptime.filterBar.filterStatusUp'); From 1c849acdd57859256d94080682ea933680c9cef0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20St=C3=BCrmer?= Date: Mon, 3 Feb 2020 16:00:17 +0100 Subject: [PATCH 30/33] [Logs UI] Add smoke tests for log rate and categories tabs (#55088) This adds function smoke tests for the new log rate and categories tabs in the Logs UI. --- x-pack/test/functional/apps/infra/index.ts | 2 + .../apps/infra/log_entry_categories_tab.ts | 28 +++++++++ .../apps/infra/log_entry_rate_tab.ts | 28 +++++++++ .../apps/infra/logs_source_configuration.ts | 62 ++++++++++++------- .../page_objects/infra_logs_page.ts | 10 +-- x-pack/test/functional/services/index.ts | 4 +- .../test/functional/services/logs_ui/index.ts | 18 ++++++ .../services/logs_ui/log_entry_categories.ts | 23 +++++++ .../services/logs_ui/log_entry_rate.ts | 23 +++++++ .../log_stream.ts} | 17 +++-- 10 files changed, 182 insertions(+), 33 deletions(-) create mode 100644 x-pack/test/functional/apps/infra/log_entry_categories_tab.ts create mode 100644 x-pack/test/functional/apps/infra/log_entry_rate_tab.ts create mode 100644 x-pack/test/functional/services/logs_ui/index.ts create mode 100644 x-pack/test/functional/services/logs_ui/log_entry_categories.ts create mode 100644 x-pack/test/functional/services/logs_ui/log_entry_rate.ts rename x-pack/test/functional/services/{infra_log_stream.ts => logs_ui/log_stream.ts} (70%) diff --git a/x-pack/test/functional/apps/infra/index.ts b/x-pack/test/functional/apps/infra/index.ts index b706dc8cce5469..597b522a88c51b 100644 --- a/x-pack/test/functional/apps/infra/index.ts +++ b/x-pack/test/functional/apps/infra/index.ts @@ -12,6 +12,8 @@ export default ({ loadTestFile }: FtrProviderContext) => { loadTestFile(require.resolve('./home_page')); loadTestFile(require.resolve('./feature_controls')); + loadTestFile(require.resolve('./log_entry_categories_tab')); + loadTestFile(require.resolve('./log_entry_rate_tab')); loadTestFile(require.resolve('./logs_source_configuration')); loadTestFile(require.resolve('./metrics_source_configuration')); loadTestFile(require.resolve('./link_to')); diff --git a/x-pack/test/functional/apps/infra/log_entry_categories_tab.ts b/x-pack/test/functional/apps/infra/log_entry_categories_tab.ts new file mode 100644 index 00000000000000..c703738e37228a --- /dev/null +++ b/x-pack/test/functional/apps/infra/log_entry_categories_tab.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default ({ getService }: FtrProviderContext) => { + const logsUi = getService('logsUi'); + const retry = getService('retry'); + + describe('Log Entry Categories Tab', function() { + this.tags('smoke'); + + describe('with a trial license', () => { + it('is visible', async () => { + await logsUi.logEntryCategoriesPage.navigateTo(); + + await retry.try(async () => { + expect(await logsUi.logEntryCategoriesPage.getSetupScreen()).to.be.ok(); + }); + }); + }); + }); +}; diff --git a/x-pack/test/functional/apps/infra/log_entry_rate_tab.ts b/x-pack/test/functional/apps/infra/log_entry_rate_tab.ts new file mode 100644 index 00000000000000..95228a520aaa25 --- /dev/null +++ b/x-pack/test/functional/apps/infra/log_entry_rate_tab.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default ({ getService }: FtrProviderContext) => { + const logsUi = getService('logsUi'); + const retry = getService('retry'); + + describe('Log Entry Rate Tab', function() { + this.tags('smoke'); + + describe('with a trial license', () => { + it('is visible', async () => { + await logsUi.logEntryRatePage.navigateTo(); + + await retry.try(async () => { + expect(await logsUi.logEntryRatePage.getSetupScreen()).to.be.ok(); + }); + }); + }); + }); +}; diff --git a/x-pack/test/functional/apps/infra/logs_source_configuration.ts b/x-pack/test/functional/apps/infra/logs_source_configuration.ts index 183acf3a980eeb..ecad5a40ec42e9 100644 --- a/x-pack/test/functional/apps/infra/logs_source_configuration.ts +++ b/x-pack/test/functional/apps/infra/logs_source_configuration.ts @@ -10,12 +10,14 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ getPageObjects, getService }: FtrProviderContext) => { const esArchiver = getService('esArchiver'); - const infraLogStream = getService('infraLogStream'); + const logsUi = getService('logsUi'); const infraSourceConfigurationForm = getService('infraSourceConfigurationForm'); const pageObjects = getPageObjects(['common', 'infraLogs']); + const retry = getService('retry'); describe('Logs Source Configuration', function() { this.tags('smoke'); + before(async () => { await esArchiver.load('empty_kibana'); }); @@ -32,8 +34,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('can change the log indices to a pattern that matches nothing', async () => { - await pageObjects.common.navigateToActualUrl('infraLogs', 'logs/settings'); - await infraSourceConfigurationForm.getForm(); + await pageObjects.infraLogs.navigateToTab('settings'); + + await retry.try(async () => { + await infraSourceConfigurationForm.getForm(); + }); const nameInput = await infraSourceConfigurationForm.getNameInput(); await nameInput.clearValueWithKeyboard({ charByChar: true }); @@ -47,13 +52,19 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('renders the no indices screen when no indices match the pattern', async () => { - await pageObjects.common.navigateToActualUrl('infraLogs', 'logs/stream'); - await pageObjects.infraLogs.getNoLogsIndicesPrompt(); + await logsUi.logStreamPage.navigateTo(); + + await retry.try(async () => { + await logsUi.logStreamPage.getNoLogsIndicesPrompt(); + }); }); it('can change the log indices back to a pattern that matches something', async () => { - await pageObjects.common.navigateToActualUrl('infraLogs', 'logs/settings'); - await infraSourceConfigurationForm.getForm(); + await pageObjects.infraLogs.navigateToTab('settings'); + + await retry.try(async () => { + await infraSourceConfigurationForm.getForm(); + }); const logIndicesInput = await infraSourceConfigurationForm.getLogIndicesInput(); await logIndicesInput.clearValueWithKeyboard({ charByChar: true }); @@ -63,16 +74,19 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('renders the default log columns with their headers', async () => { - await pageObjects.common.navigateToActualUrl('infraLogs', 'logs/stream'); - const columnHeaderLabels = await infraLogStream.getColumnHeaderLabels(); + await logsUi.logStreamPage.navigateTo(); - expect(columnHeaderLabels).to.eql(['Oct 17, 2018', 'event.dataset', 'Message']); + await retry.try(async () => { + const columnHeaderLabels = await logsUi.logStreamPage.getColumnHeaderLabels(); - const logStreamEntries = await infraLogStream.getStreamEntries(); + expect(columnHeaderLabels).to.eql(['Oct 17, 2018', 'event.dataset', 'Message']); + }); + + const logStreamEntries = await logsUi.logStreamPage.getStreamEntries(); expect(logStreamEntries.length).to.be.greaterThan(0); const firstLogStreamEntry = logStreamEntries[0]; - const logStreamEntryColumns = await infraLogStream.getLogColumnsOfStreamEntry( + const logStreamEntryColumns = await logsUi.logStreamPage.getLogColumnsOfStreamEntry( firstLogStreamEntry ); @@ -80,32 +94,34 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('can change the log columns', async () => { - await pageObjects.common.navigateToActualUrl('infraLogs', 'logs/settings'); - await infraSourceConfigurationForm.getForm(); + await pageObjects.infraLogs.navigateToTab('settings'); + + await retry.try(async () => { + await infraSourceConfigurationForm.getForm(); + }); await infraSourceConfigurationForm.removeAllLogColumns(); await infraSourceConfigurationForm.addTimestampLogColumn(); await infraSourceConfigurationForm.addFieldLogColumn('host.name'); - // await infraSourceConfigurationForm.moveLogColumn(0, 1); - await infraSourceConfigurationForm.saveConfiguration(); }); it('renders the changed log columns with their headers', async () => { - await pageObjects.common.navigateToActualUrl('infraLogs', 'logs/stream'); - const columnHeaderLabels = await infraLogStream.getColumnHeaderLabels(); + await logsUi.logStreamPage.navigateTo(); + + await retry.try(async () => { + const columnHeaderLabels = await logsUi.logStreamPage.getColumnHeaderLabels(); - // TODO: make test more robust - // expect(columnHeaderLabels).to.eql(['host.name', 'Timestamp']); - expect(columnHeaderLabels).to.eql(['Oct 17, 2018', 'host.name']); + expect(columnHeaderLabels).to.eql(['Oct 17, 2018', 'host.name']); + }); - const logStreamEntries = await infraLogStream.getStreamEntries(); + const logStreamEntries = await logsUi.logStreamPage.getStreamEntries(); expect(logStreamEntries.length).to.be.greaterThan(0); const firstLogStreamEntry = logStreamEntries[0]; - const logStreamEntryColumns = await infraLogStream.getLogColumnsOfStreamEntry( + const logStreamEntryColumns = await logsUi.logStreamPage.getLogColumnsOfStreamEntry( firstLogStreamEntry ); diff --git a/x-pack/test/functional/page_objects/infra_logs_page.ts b/x-pack/test/functional/page_objects/infra_logs_page.ts index 6eb1349210bae3..1c58f8dc41eba0 100644 --- a/x-pack/test/functional/page_objects/infra_logs_page.ts +++ b/x-pack/test/functional/page_objects/infra_logs_page.ts @@ -18,12 +18,14 @@ export function InfraLogsPageProvider({ getPageObjects, getService }: FtrProvide await pageObjects.common.navigateToApp('infraLogs'); }, - async getLogStream() { - return await testSubjects.find('logStream'); + async navigateToTab(logsUiTab: LogsUiTab) { + await pageObjects.common.navigateToActualUrl('infraLogs', `/logs/${logsUiTab}`); }, - async getNoLogsIndicesPrompt() { - return await testSubjects.find('noLogsIndicesPrompt'); + async getLogStream() { + return await testSubjects.find('logStream'); }, }; } + +type LogsUiTab = 'log-categories' | 'log-rate' | 'settings' | 'stream'; diff --git a/x-pack/test/functional/services/index.ts b/x-pack/test/functional/services/index.ts index 84d5a792ae6cae..aec91ba9e90341 100644 --- a/x-pack/test/functional/services/index.ts +++ b/x-pack/test/functional/services/index.ts @@ -46,7 +46,7 @@ import { GrokDebuggerProvider } from './grok_debugger'; import { UserMenuProvider } from './user_menu'; import { UptimeProvider } from './uptime'; import { InfraSourceConfigurationFormProvider } from './infra_source_configuration_form'; -import { InfraLogStreamProvider } from './infra_log_stream'; +import { LogsUiProvider } from './logs_ui'; import { MachineLearningProvider } from './ml'; import { TransformProvider } from './transform'; @@ -88,7 +88,7 @@ export const services = { userMenu: UserMenuProvider, uptime: UptimeProvider, infraSourceConfigurationForm: InfraSourceConfigurationFormProvider, - infraLogStream: InfraLogStreamProvider, + logsUi: LogsUiProvider, ml: MachineLearningProvider, transform: TransformProvider, }; diff --git a/x-pack/test/functional/services/logs_ui/index.ts b/x-pack/test/functional/services/logs_ui/index.ts new file mode 100644 index 00000000000000..c70a8470aafce3 --- /dev/null +++ b/x-pack/test/functional/services/logs_ui/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../../ftr_provider_context'; +import { LogEntryCategoriesPageProvider } from './log_entry_categories'; +import { LogEntryRatePageProvider } from './log_entry_rate'; +import { LogStreamPageProvider } from './log_stream'; + +export function LogsUiProvider(context: FtrProviderContext) { + return { + logEntryCategoriesPage: LogEntryCategoriesPageProvider(context), + logEntryRatePage: LogEntryRatePageProvider(context), + logStreamPage: LogStreamPageProvider(context), + }; +} diff --git a/x-pack/test/functional/services/logs_ui/log_entry_categories.ts b/x-pack/test/functional/services/logs_ui/log_entry_categories.ts new file mode 100644 index 00000000000000..b9a400b1556797 --- /dev/null +++ b/x-pack/test/functional/services/logs_ui/log_entry_categories.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { WebElementWrapper } from '../../../../../test/functional/services/lib/web_element_wrapper'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export function LogEntryCategoriesPageProvider({ getPageObjects, getService }: FtrProviderContext) { + const pageObjects = getPageObjects(['infraLogs']); + const testSubjects = getService('testSubjects'); + + return { + async navigateTo() { + pageObjects.infraLogs.navigateToTab('log-categories'); + }, + + async getSetupScreen(): Promise { + return await testSubjects.find('logEntryCategoriesSetupPage'); + }, + }; +} diff --git a/x-pack/test/functional/services/logs_ui/log_entry_rate.ts b/x-pack/test/functional/services/logs_ui/log_entry_rate.ts new file mode 100644 index 00000000000000..96c69e85aa0a48 --- /dev/null +++ b/x-pack/test/functional/services/logs_ui/log_entry_rate.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { WebElementWrapper } from '../../../../../test/functional/services/lib/web_element_wrapper'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export function LogEntryRatePageProvider({ getPageObjects, getService }: FtrProviderContext) { + const pageObjects = getPageObjects(['infraLogs']); + const testSubjects = getService('testSubjects'); + + return { + async navigateTo() { + pageObjects.infraLogs.navigateToTab('log-rate'); + }, + + async getSetupScreen(): Promise { + return await testSubjects.find('logEntryRateSetupPage'); + }, + }; +} diff --git a/x-pack/test/functional/services/infra_log_stream.ts b/x-pack/test/functional/services/logs_ui/log_stream.ts similarity index 70% rename from x-pack/test/functional/services/infra_log_stream.ts rename to x-pack/test/functional/services/logs_ui/log_stream.ts index af113d3afffb42..ce37d2d5a60daa 100644 --- a/x-pack/test/functional/services/infra_log_stream.ts +++ b/x-pack/test/functional/services/logs_ui/log_stream.ts @@ -4,14 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -import { FtrProviderContext } from '../ftr_provider_context'; -import { WebElementWrapper } from '../../../../test/functional/services/lib/web_element_wrapper'; +import { FtrProviderContext } from '../../ftr_provider_context'; +import { WebElementWrapper } from '../../../../../test/functional/services/lib/web_element_wrapper'; -export function InfraLogStreamProvider({ getService }: FtrProviderContext) { - const testSubjects = getService('testSubjects'); +export function LogStreamPageProvider({ getPageObjects, getService }: FtrProviderContext) { + const pageObjects = getPageObjects(['infraLogs']); const retry = getService('retry'); + const testSubjects = getService('testSubjects'); return { + async navigateTo() { + pageObjects.infraLogs.navigateToTab('stream'); + }, + async getColumnHeaderLabels(): Promise { const columnHeaderElements: WebElementWrapper[] = await testSubjects.findAll( '~logColumnHeader' @@ -35,5 +40,9 @@ export function InfraLogStreamProvider({ getService }: FtrProviderContext) { ): Promise { return await testSubjects.findAllDescendant('~logColumn', entryElement); }, + + async getNoLogsIndicesPrompt() { + return await testSubjects.find('noLogsIndicesPrompt'); + }, }; } From 85988386b6fcc5f71c0b01fd3b6fd14767bdb93d Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Mon, 3 Feb 2020 07:04:23 -0800 Subject: [PATCH 31/33] Containers (#56571) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 💡 move state containers to /common folder * refactor: 💡 remove RecursiveReadonly type in state containers * fix: 🐛 assume we are in production by default on server-side Co-authored-by: Elastic Machine --- src/plugins/kibana_utils/common/index.ts | 1 + .../create_state_container.test.ts | 0 .../create_state_container.ts | 25 +++++++++++-------- ...ate_state_container_react_helpers.test.tsx | 0 .../create_state_container_react_helpers.ts | 0 .../state_containers/index.ts | 0 .../state_containers/types.ts | 18 ++++++------- .../demos/state_containers/counter.ts | 2 +- .../demos/state_containers/todomvc.ts | 2 +- .../kibana_utils/demos/state_sync/url.ts | 2 +- src/plugins/kibana_utils/public/index.ts | 2 +- .../public/state_sync/state_sync.test.ts | 2 +- .../public/state_sync/state_sync.ts | 2 +- .../kibana_utils/public/state_sync/types.ts | 2 +- 14 files changed, 31 insertions(+), 27 deletions(-) rename src/plugins/kibana_utils/{public => common}/state_containers/create_state_container.test.ts (100%) rename src/plugins/kibana_utils/{public => common}/state_containers/create_state_container.ts (86%) rename src/plugins/kibana_utils/{public => common}/state_containers/create_state_container_react_helpers.test.tsx (100%) rename src/plugins/kibana_utils/{public => common}/state_containers/create_state_container_react_helpers.ts (100%) rename src/plugins/kibana_utils/{public => common}/state_containers/index.ts (100%) rename src/plugins/kibana_utils/{public => common}/state_containers/types.ts (88%) diff --git a/src/plugins/kibana_utils/common/index.ts b/src/plugins/kibana_utils/common/index.ts index bfb45b88964d89..d4aeb2c0fe4ad4 100644 --- a/src/plugins/kibana_utils/common/index.ts +++ b/src/plugins/kibana_utils/common/index.ts @@ -19,4 +19,5 @@ export * from './defer'; export * from './of'; +export * from './state_containers'; export { distinctUntilChangedWithInitialValue } from './distinct_until_changed_with_initial_value'; diff --git a/src/plugins/kibana_utils/public/state_containers/create_state_container.test.ts b/src/plugins/kibana_utils/common/state_containers/create_state_container.test.ts similarity index 100% rename from src/plugins/kibana_utils/public/state_containers/create_state_container.test.ts rename to src/plugins/kibana_utils/common/state_containers/create_state_container.test.ts diff --git a/src/plugins/kibana_utils/public/state_containers/create_state_container.ts b/src/plugins/kibana_utils/common/state_containers/create_state_container.ts similarity index 86% rename from src/plugins/kibana_utils/public/state_containers/create_state_container.ts rename to src/plugins/kibana_utils/common/state_containers/create_state_container.ts index d420aec30f068c..78bfc0c3e90906 100644 --- a/src/plugins/kibana_utils/public/state_containers/create_state_container.ts +++ b/src/plugins/kibana_utils/common/state_containers/create_state_container.ts @@ -19,7 +19,6 @@ import { BehaviorSubject } from 'rxjs'; import { skip } from 'rxjs/operators'; -import { RecursiveReadonly } from '@kbn/utility-types'; import deepFreeze from 'deep-freeze-strict'; import { PureTransitionsToTransitions, @@ -32,14 +31,18 @@ import { const $$observable = (typeof Symbol === 'function' && (Symbol as any).observable) || '@@observable'; const $$setActionType = '@@SET'; -const freeze: (value: T) => RecursiveReadonly = - process.env.NODE_ENV !== 'production' - ? (value: T): RecursiveReadonly => { - const isFreezable = value !== null && typeof value === 'object'; - if (isFreezable) return deepFreeze(value) as RecursiveReadonly; - return value as RecursiveReadonly; - } - : (value: T) => value as RecursiveReadonly; +const isProduction = + typeof window === 'object' + ? process.env.NODE_ENV === 'production' + : !process.env.NODE_ENV || process.env.NODE_ENV === 'production'; + +const freeze: (value: T) => T = isProduction + ? (value: T) => value as T + : (value: T): T => { + const isFreezable = value !== null && typeof value === 'object'; + if (isFreezable) return deepFreeze(value) as T; + return value as T; + }; export function createStateContainer( defaultState: State @@ -66,7 +69,7 @@ export function createStateContainer< pureTransitions: PureTransitions = {} as PureTransitions, pureSelectors: PureSelectors = {} as PureSelectors ): ReduxLikeStateContainer { - const data$ = new BehaviorSubject>(freeze(defaultState)); + const data$ = new BehaviorSubject(freeze(defaultState)); const state$ = data$.pipe(skip(1)); const get = () => data$.getValue(); const container: ReduxLikeStateContainer = { @@ -101,7 +104,7 @@ export function createStateContainer< ), addMiddleware: middleware => (container.dispatch = middleware(container as any)(container.dispatch)), - subscribe: (listener: (state: RecursiveReadonly) => void) => { + subscribe: (listener: (state: State) => void) => { const subscription = state$.subscribe(listener); return () => subscription.unsubscribe(); }, diff --git a/src/plugins/kibana_utils/public/state_containers/create_state_container_react_helpers.test.tsx b/src/plugins/kibana_utils/common/state_containers/create_state_container_react_helpers.test.tsx similarity index 100% rename from src/plugins/kibana_utils/public/state_containers/create_state_container_react_helpers.test.tsx rename to src/plugins/kibana_utils/common/state_containers/create_state_container_react_helpers.test.tsx diff --git a/src/plugins/kibana_utils/public/state_containers/create_state_container_react_helpers.ts b/src/plugins/kibana_utils/common/state_containers/create_state_container_react_helpers.ts similarity index 100% rename from src/plugins/kibana_utils/public/state_containers/create_state_container_react_helpers.ts rename to src/plugins/kibana_utils/common/state_containers/create_state_container_react_helpers.ts diff --git a/src/plugins/kibana_utils/public/state_containers/index.ts b/src/plugins/kibana_utils/common/state_containers/index.ts similarity index 100% rename from src/plugins/kibana_utils/public/state_containers/index.ts rename to src/plugins/kibana_utils/common/state_containers/index.ts diff --git a/src/plugins/kibana_utils/public/state_containers/types.ts b/src/plugins/kibana_utils/common/state_containers/types.ts similarity index 88% rename from src/plugins/kibana_utils/public/state_containers/types.ts rename to src/plugins/kibana_utils/common/state_containers/types.ts index 5f27a3d2c1dcad..26a29bc470e8ac 100644 --- a/src/plugins/kibana_utils/public/state_containers/types.ts +++ b/src/plugins/kibana_utils/common/state_containers/types.ts @@ -18,7 +18,7 @@ */ import { Observable } from 'rxjs'; -import { Ensure, RecursiveReadonly } from '@kbn/utility-types'; +import { Ensure } from '@kbn/utility-types'; export type BaseState = object; export interface TransitionDescription { @@ -27,7 +27,7 @@ export interface TransitionDescription = (...args: Args) => State; export type PureTransition = ( - state: RecursiveReadonly + state: State ) => Transition; export type EnsurePureTransition = Ensure>; export type PureTransitionToTransition> = ReturnType; @@ -36,9 +36,9 @@ export type PureTransitionsToTransitions = { }; export interface BaseStateContainer { - get: () => RecursiveReadonly; + get: () => State; set: (state: State) => void; - state$: Observable>; + state$: Observable; } export interface StateContainer< @@ -55,12 +55,12 @@ export interface ReduxLikeStateContainer< PureTransitions extends object = {}, PureSelectors extends object = {} > extends StateContainer { - getState: () => RecursiveReadonly; - reducer: Reducer>; - replaceReducer: (nextReducer: Reducer>) => void; + getState: () => State; + reducer: Reducer; + replaceReducer: (nextReducer: Reducer) => void; dispatch: (action: TransitionDescription) => void; - addMiddleware: (middleware: Middleware>) => void; - subscribe: (listener: (state: RecursiveReadonly) => void) => () => void; + addMiddleware: (middleware: Middleware) => void; + subscribe: (listener: (state: State) => void) => () => void; } export type Dispatch = (action: T) => void; diff --git a/src/plugins/kibana_utils/demos/state_containers/counter.ts b/src/plugins/kibana_utils/demos/state_containers/counter.ts index 4ddf532c1506db..0484a906a60d3b 100644 --- a/src/plugins/kibana_utils/demos/state_containers/counter.ts +++ b/src/plugins/kibana_utils/demos/state_containers/counter.ts @@ -17,7 +17,7 @@ * under the License. */ -import { createStateContainer } from '../../public/state_containers'; +import { createStateContainer } from '../../common/state_containers'; interface State { count: number; diff --git a/src/plugins/kibana_utils/demos/state_containers/todomvc.ts b/src/plugins/kibana_utils/demos/state_containers/todomvc.ts index e807783a56f319..0a07d721479b3b 100644 --- a/src/plugins/kibana_utils/demos/state_containers/todomvc.ts +++ b/src/plugins/kibana_utils/demos/state_containers/todomvc.ts @@ -17,7 +17,7 @@ * under the License. */ -import { createStateContainer, PureTransition } from '../../public/state_containers'; +import { createStateContainer, PureTransition } from '../../common/state_containers'; export interface TodoItem { text: string; diff --git a/src/plugins/kibana_utils/demos/state_sync/url.ts b/src/plugins/kibana_utils/demos/state_sync/url.ts index 2c426cae6733a4..80c016950d2242 100644 --- a/src/plugins/kibana_utils/demos/state_sync/url.ts +++ b/src/plugins/kibana_utils/demos/state_sync/url.ts @@ -18,7 +18,7 @@ */ import { defaultState, pureTransitions, TodoActions, TodoState } from '../state_containers/todomvc'; -import { BaseState, BaseStateContainer, createStateContainer } from '../../public/state_containers'; +import { BaseState, BaseStateContainer, createStateContainer } from '../../common/state_containers'; import { createKbnUrlStateStorage, syncState, diff --git a/src/plugins/kibana_utils/public/index.ts b/src/plugins/kibana_utils/public/index.ts index 00c1c95028b4dc..78828ad9c4b2de 100644 --- a/src/plugins/kibana_utils/public/index.ts +++ b/src/plugins/kibana_utils/public/index.ts @@ -25,7 +25,7 @@ export * from './field_wildcard'; export * from './parse'; export * from './render_complete'; export * from './resize_checker'; -export * from './state_containers'; +export * from '../common/state_containers'; export * from './storage'; export { hashedItemStore, HashedItemStore } from './storage/hashed_item_store'; export { diff --git a/src/plugins/kibana_utils/public/state_sync/state_sync.test.ts b/src/plugins/kibana_utils/public/state_sync/state_sync.test.ts index 17f41483a0a21e..c55c60f9b0f890 100644 --- a/src/plugins/kibana_utils/public/state_sync/state_sync.test.ts +++ b/src/plugins/kibana_utils/public/state_sync/state_sync.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { BaseState, BaseStateContainer, createStateContainer } from '../state_containers'; +import { BaseState, BaseStateContainer, createStateContainer } from '../../common/state_containers'; import { defaultState, pureTransitions, diff --git a/src/plugins/kibana_utils/public/state_sync/state_sync.ts b/src/plugins/kibana_utils/public/state_sync/state_sync.ts index 28d133829e07c4..ed57723f8f2b7b 100644 --- a/src/plugins/kibana_utils/public/state_sync/state_sync.ts +++ b/src/plugins/kibana_utils/public/state_sync/state_sync.ts @@ -23,7 +23,7 @@ import defaultComparator from 'fast-deep-equal'; import { IStateSyncConfig } from './types'; import { IStateStorage } from './state_sync_state_storage'; import { distinctUntilChangedWithInitialValue } from '../../common'; -import { BaseState } from '../state_containers'; +import { BaseState } from '../../common/state_containers'; import { applyDiff } from '../state_management/utils/diff_object'; /** diff --git a/src/plugins/kibana_utils/public/state_sync/types.ts b/src/plugins/kibana_utils/public/state_sync/types.ts index 3009c1d161a532..2acb466d92e920 100644 --- a/src/plugins/kibana_utils/public/state_sync/types.ts +++ b/src/plugins/kibana_utils/public/state_sync/types.ts @@ -17,7 +17,7 @@ * under the License. */ -import { BaseState, BaseStateContainer } from '../state_containers/types'; +import { BaseState, BaseStateContainer } from '../../common/state_containers/types'; import { IStateStorage } from './state_sync_state_storage'; export interface INullableBaseStateContainer From c86ee1b6ea001863fde399721febaf1704fd2fc0 Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Mon, 3 Feb 2020 07:05:39 -0800 Subject: [PATCH 32/33] =?UTF-8?q?chore:=20=F0=9F=A4=96=20add=20AppArch=20p?= =?UTF-8?q?lugins=20to=20CODEOWNERS=20(#56397)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: 🤖 add AppArch plugins to CODEOWNERS And sort the list alphabetically. * chore: 🤖 add @kbn/interpreter to AppArch CODEOWNERS Co-authored-by: Elastic Machine --- .github/CODEOWNERS | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 2e2b20a46baede..0b0addf117f6fc 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -28,15 +28,7 @@ /src/plugins/dev_tools/ @elastic/kibana-app # App Architecture -/src/plugins/data/ @elastic/kibana-app-arch -/src/plugins/embeddable/ @elastic/kibana-app-arch -/src/plugins/expressions/ @elastic/kibana-app-arch -/src/plugins/kibana_react/ @elastic/kibana-app-arch -/src/plugins/kibana_utils/ @elastic/kibana-app-arch -/src/plugins/navigation/ @elastic/kibana-app-arch -/src/plugins/ui_actions/ @elastic/kibana-app-arch -/src/plugins/visualizations/ @elastic/kibana-app-arch -/x-pack/plugins/advanced_ui_actions/ @elastic/kibana-app-arch +/packages/kbn-interpreter/ @elastic/kibana-app-arch /src/legacy/core_plugins/data/ @elastic/kibana-app-arch /src/legacy/core_plugins/elasticsearch/lib/create_proxy.js @elastic/kibana-app-arch /src/legacy/core_plugins/embeddable_api/ @elastic/kibana-app-arch @@ -48,6 +40,19 @@ /src/legacy/core_plugins/kibana/server/routes/api/suggestions/ @elastic/kibana-app-arch /src/legacy/core_plugins/visualizations/ @elastic/kibana-app-arch /src/legacy/server/index_patterns/ @elastic/kibana-app-arch +/src/plugins/bfetch/ @elastic/kibana-app-arch +/src/plugins/dashboard_embeddable_container/ @elastic/kibana-app-arch +/src/plugins/data/ @elastic/kibana-app-arch +/src/plugins/embeddable/ @elastic/kibana-app-arch +/src/plugins/expressions/ @elastic/kibana-app-arch +/src/plugins/inspector/ @elastic/kibana-app-arch +/src/plugins/kibana_react/ @elastic/kibana-app-arch +/src/plugins/kibana_utils/ @elastic/kibana-app-arch +/src/plugins/management/ @elastic/kibana-app-arch +/src/plugins/navigation/ @elastic/kibana-app-arch +/src/plugins/ui_actions/ @elastic/kibana-app-arch +/src/plugins/visualizations/ @elastic/kibana-app-arch +/x-pack/plugins/advanced_ui_actions/ @elastic/kibana-app-arch # APM /x-pack/legacy/plugins/apm/ @elastic/apm-ui From 95e40e7fa3a7be1b9818c67177f3e8f4927eb9ba Mon Sep 17 00:00:00 2001 From: Shahzad Date: Mon, 3 Feb 2020 16:22:38 +0100 Subject: [PATCH 33/33] [Uptime] Refresh absolute date ranges for Ping Histogram (#56381) * fix abs date mismatch * fixed types * update pr * simplify params Co-authored-by: Elastic Machine --- .../connected/charts/ping_histogram.tsx | 36 +- .../connected/charts/snapshot_container.tsx | 95 +++ .../filter_group/filter_group_container.tsx | 17 +- .../public/components/connected/index.ts | 3 + .../monitor/status_bar_container.tsx | 85 +++ .../monitor/status_details_container.tsx | 64 ++ .../monitor_charts.test.tsx.snap | 2 - .../__tests__/monitor_charts.test.tsx | 2 - .../functional/__tests__/snapshot.test.tsx | 4 +- .../monitor_bar_series.test.tsx.snap | 156 +++-- .../__tests__/monitor_bar_series.test.tsx | 159 ++++- .../functional/charts/monitor_bar_series.tsx | 25 +- .../public/components/functional/index.ts | 2 - .../components/functional/monitor_charts.tsx | 26 +- .../__snapshots__/monitor_list.test.tsx.snap | 554 ++++++++++++++++++ .../monitor_list_pagination.test.tsx.snap | 4 +- .../__tests__/monitor_list.test.tsx | 25 +- .../monitor_list_pagination.test.tsx | 6 +- .../functional/monitor_list/monitor_list.tsx | 20 +- .../monitor_status.bar.test.tsx.snap | 0 .../__test__}/monitor_status.bar.test.tsx | 12 +- .../monitor_status_details/index.ts | 31 +- .../monitor_status_bar/index.ts | 44 +- .../monitor_status_bar/monitor_status_bar.tsx | 26 +- .../monitor_status_details.tsx | 27 +- .../public/components/functional/snapshot.tsx | 107 +--- .../components/functional/status_panel.tsx | 42 +- .../public/hooks/update_kuery_string.ts | 4 +- .../plugins/uptime/public/pages/monitor.tsx | 17 +- .../plugins/uptime/public/pages/overview.tsx | 11 +- 30 files changed, 1148 insertions(+), 458 deletions(-) create mode 100644 x-pack/legacy/plugins/uptime/public/components/connected/charts/snapshot_container.tsx create mode 100644 x-pack/legacy/plugins/uptime/public/components/connected/monitor/status_bar_container.tsx create mode 100644 x-pack/legacy/plugins/uptime/public/components/connected/monitor/status_details_container.tsx rename x-pack/legacy/plugins/uptime/public/components/functional/{__tests__ => monitor_status_details/__test__}/__snapshots__/monitor_status.bar.test.tsx.snap (100%) rename x-pack/legacy/plugins/uptime/public/components/functional/{__tests__ => monitor_status_details/__test__}/monitor_status.bar.test.tsx (79%) diff --git a/x-pack/legacy/plugins/uptime/public/components/connected/charts/ping_histogram.tsx b/x-pack/legacy/plugins/uptime/public/components/connected/charts/ping_histogram.tsx index a6607ca81fc183..cbdd921a36e81b 100644 --- a/x-pack/legacy/plugins/uptime/public/components/connected/charts/ping_histogram.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/connected/charts/ping_histogram.tsx @@ -15,34 +15,40 @@ import { getPingHistogram } from '../../../state/actions'; import { selectPingHistogram } from '../../../state/selectors'; import { withResponsiveWrapper, ResponsiveWrapperProps } from '../../higher_order'; import { GetPingHistogramParams, HistogramResult } from '../../../../common/types'; +import { useUrlParams } from '../../../hooks'; -type Props = GetPingHistogramParams & - ResponsiveWrapperProps & - PingHistogramComponentProps & - DispatchProps & { lastRefresh: number }; +type Props = ResponsiveWrapperProps & + Pick & + DispatchProps & { lastRefresh: number; monitorId?: string }; const PingHistogramContainer: React.FC = ({ data, loadData, - statusFilter, - filters, - dateStart, - dateEnd, - absoluteStartDate, - absoluteEndDate, monitorId, lastRefresh, - ...props + height, + loading, }) => { + const [getUrlParams] = useUrlParams(); + const { + absoluteDateRangeStart, + absoluteDateRangeEnd, + dateRangeStart: dateStart, + dateRangeEnd: dateEnd, + statusFilter, + filters, + } = getUrlParams(); + useEffect(() => { loadData({ monitorId, dateStart, dateEnd, statusFilter, filters }); }, [loadData, dateStart, dateEnd, monitorId, filters, statusFilter, lastRefresh]); return ( ); }; @@ -68,7 +74,7 @@ const mapDispatchToProps = (dispatch: any): DispatchProps => ({ export const PingHistogram = connect< StateProps, DispatchProps, - PingHistogramComponentProps, + Pick, AppState >( mapStateToProps, diff --git a/x-pack/legacy/plugins/uptime/public/components/connected/charts/snapshot_container.tsx b/x-pack/legacy/plugins/uptime/public/components/connected/charts/snapshot_container.tsx new file mode 100644 index 00000000000000..6d01ebae1e100f --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/connected/charts/snapshot_container.tsx @@ -0,0 +1,95 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useEffect } from 'react'; +import { connect } from 'react-redux'; +import { useUrlParams } from '../../../hooks'; +import { AppState } from '../../../state'; +import { fetchSnapshotCount } from '../../../state/actions'; +import { SnapshotComponent } from '../../functional/snapshot'; +import { Snapshot as SnapshotType } from '../../../../common/runtime_types'; + +/** + * Props expected from parent components. + */ +interface OwnProps { + /** + * Height is needed, since by default charts takes height of 100% + */ + height?: string; +} + +/** + * Props given by the Redux store based on action input. + */ +interface StoreProps { + count: SnapshotType; + lastRefresh: number; + loading: boolean; +} + +/** + * Contains functions that will dispatch actions used + * for this component's life cycle + */ +interface DispatchProps { + loadSnapshotCount: typeof fetchSnapshotCount; +} + +/** + * Props used to render the Snapshot component. + */ +type Props = OwnProps & StoreProps & DispatchProps; + +export const Container: React.FC = ({ + count, + height, + lastRefresh, + loading, + loadSnapshotCount, +}: Props) => { + const [getUrlParams] = useUrlParams(); + const { dateRangeStart, dateRangeEnd, statusFilter, filters } = getUrlParams(); + + useEffect(() => { + loadSnapshotCount(dateRangeStart, dateRangeEnd, filters, statusFilter); + }, [dateRangeStart, dateRangeEnd, filters, lastRefresh, loadSnapshotCount, statusFilter]); + return ; +}; + +/** + * Provides state to connected component. + * @param state the root app state + */ +const mapStateToProps = ({ + snapshot: { count, loading }, + ui: { lastRefresh }, +}: AppState): StoreProps => ({ + count, + lastRefresh, + loading, +}); + +/** + * Used for fetching snapshot counts. + * @param dispatch redux-provided action dispatcher + */ +const mapDispatchToProps = (dispatch: any) => ({ + loadSnapshotCount: ( + dateRangeStart: string, + dateRangeEnd: string, + filters?: string, + statusFilter?: string + ): DispatchProps => { + return dispatch(fetchSnapshotCount(dateRangeStart, dateRangeEnd, filters, statusFilter)); + }, +}); + +export const Snapshot = connect( + // @ts-ignore connect is expecting null | undefined for some reason + mapStateToProps, + mapDispatchToProps +)(Container); diff --git a/x-pack/legacy/plugins/uptime/public/components/connected/filter_group/filter_group_container.tsx b/x-pack/legacy/plugins/uptime/public/components/connected/filter_group/filter_group_container.tsx index 2d1c21d1c997da..569c6bb883cbdc 100644 --- a/x-pack/legacy/plugins/uptime/public/components/connected/filter_group/filter_group_container.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/connected/filter_group/filter_group_container.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useEffect } from 'react'; +import React, { useContext, useEffect } from 'react'; import { connect } from 'react-redux'; import { useUrlParams } from '../../../hooks'; import { parseFiltersMap } from '../../functional/filter_group/parse_filter_map'; @@ -12,6 +12,7 @@ import { AppState } from '../../../state'; import { fetchOverviewFilters, GetOverviewFiltersPayload } from '../../../state/actions'; import { FilterGroupComponent } from '../../functional/filter_group'; import { OverviewFilters } from '../../../../common/runtime_types/overview_filters'; +import { UptimeRefreshContext } from '../../../contexts'; interface OwnProps { esFilters?: string; @@ -37,8 +38,9 @@ export const Container: React.FC = ({ loadFilterGroup, overviewFilters, }: Props) => { - const [getUrlParams, updateUrl] = useUrlParams(); + const { lastRefresh } = useContext(UptimeRefreshContext); + const [getUrlParams, updateUrl] = useUrlParams(); const { dateRangeStart, dateRangeEnd, statusFilter, filters: urlFilters } = getUrlParams(); useEffect(() => { @@ -53,7 +55,16 @@ export const Container: React.FC = ({ statusFilter, tags: filterSelections.tags ?? [], }); - }, [dateRangeStart, dateRangeEnd, esKuery, esFilters, statusFilter, urlFilters, loadFilterGroup]); + }, [ + lastRefresh, + dateRangeStart, + dateRangeEnd, + esKuery, + esFilters, + statusFilter, + urlFilters, + loadFilterGroup, + ]); // update filters in the URL from filter group const onFilterUpdate = (filtersKuery: string) => { diff --git a/x-pack/legacy/plugins/uptime/public/components/connected/index.ts b/x-pack/legacy/plugins/uptime/public/components/connected/index.ts index 5bb0d1ae8468f9..2fd4c762cf45f5 100644 --- a/x-pack/legacy/plugins/uptime/public/components/connected/index.ts +++ b/x-pack/legacy/plugins/uptime/public/components/connected/index.ts @@ -5,6 +5,9 @@ */ export { PingHistogram } from './charts/ping_histogram'; +export { Snapshot } from './charts/snapshot_container'; export { KueryBar } from './kuerybar/kuery_bar_container'; export { OverviewPage } from './pages/overview_container'; export { FilterGroup } from './filter_group/filter_group_container'; +export { MonitorStatusDetails } from './monitor/status_details_container'; +export { MonitorStatusBar } from './monitor/status_bar_container'; diff --git a/x-pack/legacy/plugins/uptime/public/components/connected/monitor/status_bar_container.tsx b/x-pack/legacy/plugins/uptime/public/components/connected/monitor/status_bar_container.tsx new file mode 100644 index 00000000000000..db6337732091a9 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/connected/monitor/status_bar_container.tsx @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useContext, useEffect } from 'react'; +import { connect } from 'react-redux'; +import { Dispatch } from 'redux'; +import { AppState } from '../../../state'; +import { selectMonitorLocations, selectMonitorStatus } from '../../../state/selectors'; +import { MonitorStatusBarComponent } from '../../functional/monitor_status_details/monitor_status_bar'; +import { getMonitorStatus, getSelectedMonitor } from '../../../state/actions'; +import { useUrlParams } from '../../../hooks'; +import { Ping } from '../../../../common/graphql/types'; +import { MonitorLocations } from '../../../../common/runtime_types/monitor'; +import { UptimeRefreshContext } from '../../../contexts'; + +interface StateProps { + monitorStatus: Ping; + monitorLocations: MonitorLocations; +} + +interface DispatchProps { + loadMonitorStatus: (dateStart: string, dateEnd: string, monitorId: string) => void; +} + +interface OwnProps { + monitorId: string; +} + +type Props = OwnProps & StateProps & DispatchProps; + +export const Container: React.FC = ({ + loadMonitorStatus, + monitorId, + monitorStatus, + monitorLocations, +}: Props) => { + const { lastRefresh } = useContext(UptimeRefreshContext); + + const [getUrlParams] = useUrlParams(); + const { dateRangeStart: dateStart, dateRangeEnd: dateEnd } = getUrlParams(); + + useEffect(() => { + loadMonitorStatus(dateStart, dateEnd, monitorId); + }, [monitorId, dateStart, dateEnd, loadMonitorStatus, lastRefresh]); + + return ( + + ); +}; + +const mapStateToProps = (state: AppState, ownProps: OwnProps) => ({ + monitorStatus: selectMonitorStatus(state), + monitorLocations: selectMonitorLocations(state, ownProps.monitorId), +}); + +const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({ + loadMonitorStatus: (dateStart: string, dateEnd: string, monitorId: string) => { + dispatch( + getMonitorStatus({ + monitorId, + dateStart, + dateEnd, + }) + ); + dispatch( + getSelectedMonitor({ + monitorId, + }) + ); + }, +}); + +// @ts-ignore TODO: Investigate typescript issues here +export const MonitorStatusBar = connect( + // @ts-ignore TODO: Investigate typescript issues here + mapStateToProps, + mapDispatchToProps +)(Container); diff --git a/x-pack/legacy/plugins/uptime/public/components/connected/monitor/status_details_container.tsx b/x-pack/legacy/plugins/uptime/public/components/connected/monitor/status_details_container.tsx new file mode 100644 index 00000000000000..6929e3bd64c4d0 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/connected/monitor/status_details_container.tsx @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useContext, useEffect } from 'react'; +import { connect } from 'react-redux'; +import { Dispatch } from 'redux'; +import { useUrlParams } from '../../../hooks'; +import { AppState } from '../../../state'; +import { selectMonitorLocations } from '../../../state/selectors'; +import { fetchMonitorLocations, MonitorLocationsPayload } from '../../../state/actions/monitor'; +import { MonitorStatusDetailsComponent } from '../../functional/monitor_status_details'; +import { MonitorLocations } from '../../../../common/runtime_types'; +import { UptimeRefreshContext } from '../../../contexts'; + +interface OwnProps { + monitorId: string; +} + +interface StoreProps { + monitorLocations: MonitorLocations; +} + +interface DispatchProps { + loadMonitorLocations: typeof fetchMonitorLocations; +} + +type Props = OwnProps & StoreProps & DispatchProps; + +export const Container: React.FC = ({ + loadMonitorLocations, + monitorLocations, + monitorId, +}: Props) => { + const { lastRefresh } = useContext(UptimeRefreshContext); + + const [getUrlParams] = useUrlParams(); + const { dateRangeStart: dateStart, dateRangeEnd: dateEnd } = getUrlParams(); + + useEffect(() => { + loadMonitorLocations({ dateStart, dateEnd, monitorId }); + }, [loadMonitorLocations, monitorId, dateStart, dateEnd, lastRefresh]); + + return ( + + ); +}; +const mapStateToProps = (state: AppState, { monitorId }: OwnProps) => ({ + monitorLocations: selectMonitorLocations(state, monitorId), +}); + +const mapDispatchToProps = (dispatch: Dispatch) => ({ + loadMonitorLocations: (params: MonitorLocationsPayload) => { + dispatch(fetchMonitorLocations(params)); + }, +}); + +export const MonitorStatusDetails = connect( + // @ts-ignore TODO: Investigate typescript issues here + mapStateToProps, + mapDispatchToProps +)(Container); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/monitor_charts.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/monitor_charts.test.tsx.snap index f6846dfb1164da..9853ed5cadfc9b 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/monitor_charts.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/monitor_charts.test.tsx.snap @@ -180,8 +180,6 @@ exports[`MonitorCharts component renders the component without errors 1`] = ` }, } } - dateRangeEnd="2011-12-03T10:15:30+01:00" - dateRangeStart="2011-12-03T10:15:30+01:00" loading={false} mean="mean" monitorId="something" diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/monitor_charts.test.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/monitor_charts.test.tsx index 81c60c8fbeaaa9..331b5c9c0b0965 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/monitor_charts.test.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/monitor_charts.test.tsx @@ -73,8 +73,6 @@ describe('MonitorCharts component', () => { range="range" success="success" monitorId="something" - dateRangeStart="2011-12-03T10:15:30+01:00" - dateRangeEnd="2011-12-03T10:15:30+01:00" /> ) ); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/snapshot.test.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/snapshot.test.tsx index d645eb21ac7766..214b0394369f7a 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/snapshot.test.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/snapshot.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { shallowWithIntl } from 'test_utils/enzyme_helpers'; import { Snapshot } from '../../../../common/runtime_types'; -import { PresentationalComponent } from '../snapshot'; +import { SnapshotComponent } from '../snapshot'; describe('Snapshot component', () => { const snapshot: Snapshot = { @@ -17,7 +17,7 @@ describe('Snapshot component', () => { }; it('renders without errors', () => { - const wrapper = shallowWithIntl(); + const wrapper = shallowWithIntl(); expect(wrapper).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/monitor_bar_series.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/monitor_bar_series.test.tsx.snap index c3b99c9785cbe0..8ca73879cab8c0 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/monitor_bar_series.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/monitor_bar_series.test.tsx.snap @@ -1,67 +1,107 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`MonitorBarSeries component renders a series when there are down items 1`] = ` +exports[`MonitorBarSeries component renders if the data series is present 1`] = `
- - - - - +
+
+

+ No data to display +

+
+
+
`; + +exports[`MonitorBarSeries component shallow renders a series when there are down items 1`] = ` + + + +`; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/monitor_bar_series.test.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/monitor_bar_series.test.tsx index 3cede0be00ef1c..c3e98134e438d8 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/monitor_bar_series.test.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/monitor_bar_series.test.tsx @@ -5,15 +5,16 @@ */ import React from 'react'; -import { shallowWithIntl } from 'test_utils/enzyme_helpers'; +import { renderWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; import { MonitorBarSeries, MonitorBarSeriesProps } from '../monitor_bar_series'; +import { renderWithRouter } from '../../../../lib'; +import { SummaryHistogramPoint } from '../../../../../common/graphql/types'; describe('MonitorBarSeries component', () => { let props: MonitorBarSeriesProps; + let histogramSeries: SummaryHistogramPoint[]; beforeEach(() => { props = { - absoluteStartDate: 1548697920000, - absoluteEndDate: 1548700920000, dangerColor: 'A danger color', histogramSeries: [ { @@ -33,20 +34,144 @@ describe('MonitorBarSeries component', () => { }, ], }; + histogramSeries = [ + { timestamp: 1580387868000, up: 0, down: 5 }, + { timestamp: 1580387904000, up: 0, down: 20 }, + { + timestamp: 1580387940000, + up: 0, + down: 19, + }, + { + timestamp: 1580387976000, + up: 0, + down: 16, + }, + { + timestamp: 1580388012000, + up: 0, + down: 20, + }, + { + timestamp: 1580388048000, + up: 0, + down: 15, + }, + { + timestamp: 1580388084000, + up: 0, + down: 20, + }, + { + timestamp: 1580388120000, + up: 0, + down: 19, + }, + { + timestamp: 1580388156000, + up: 0, + down: 16, + }, + { + timestamp: 1580388192000, + up: 0, + down: 20, + }, + { + timestamp: 1580388228000, + up: 0, + down: 15, + }, + { + timestamp: 1580388264000, + up: 0, + down: 20, + }, + { + timestamp: 1580388300000, + up: 0, + down: 19, + }, + { + timestamp: 1580388336000, + up: 0, + down: 16, + }, + { + timestamp: 1580388372000, + up: 0, + down: 20, + }, + { + timestamp: 1580388408000, + up: 0, + down: 15, + }, + { + timestamp: 1580388444000, + up: 0, + down: 20, + }, + { + timestamp: 1580388480000, + up: 0, + down: 19, + }, + { + timestamp: 1580388516000, + up: 0, + down: 16, + }, + { + timestamp: 1580388552000, + up: 0, + down: 20, + }, + { + timestamp: 1580388588000, + up: 0, + down: 15, + }, + { + timestamp: 1580388624000, + up: 0, + down: 20, + }, + { + timestamp: 1580388660000, + up: 0, + down: 19, + }, + { + timestamp: 1580388696000, + up: 0, + down: 16, + }, + { + timestamp: 1580388732000, + up: 0, + down: 20, + }, + { + timestamp: 1580388768000, + up: 0, + down: 10, + }, + ]; }); - it('renders a series when there are down items', () => { - const component = shallowWithIntl(); + it('shallow renders a series when there are down items', () => { + const component = shallowWithIntl(renderWithRouter()); expect(component).toMatchSnapshot(); }); - it('renders null when there are no down items', () => { + it('shallow renders null when there are no down items', () => { props.histogramSeries = []; - const component = shallowWithIntl(); + const component = shallowWithIntl(renderWithRouter()); expect(component).toEqual({}); }); - it('renders nothing if the down count has no counts', () => { + it(' shallow renders nothing if the down count has no counts', () => { props.histogramSeries = [ { timestamp: 123, @@ -64,19 +189,21 @@ describe('MonitorBarSeries component', () => { up: 0, }, ]; - const component = shallowWithIntl(); + const component = shallowWithIntl(renderWithRouter()); expect(component).toEqual({}); }); - it('renders nothing if the data series is null', () => { + it('shallow renders nothing if the data series is null', () => { const component = shallowWithIntl( - + renderWithRouter() ); expect(component).toEqual({}); }); + + it('renders if the data series is present', () => { + const component = renderWithIntl( + renderWithRouter() + ); + expect(component).toMatchSnapshot(); + }); }); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/monitor_bar_series.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/charts/monitor_bar_series.tsx index ce91bf5b1638f4..2338bf02783485 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/charts/monitor_bar_series.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/charts/monitor_bar_series.tsx @@ -19,16 +19,9 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { EuiText, EuiToolTip } from '@elastic/eui'; import { SummaryHistogramPoint } from '../../../../common/graphql/types'; import { getChartDateLabel, seriesHasDownValues } from '../../../lib/helper'; +import { useUrlParams } from '../../../hooks'; export interface MonitorBarSeriesProps { - /** - * The date/time for the start of the timespan. - */ - absoluteStartDate: number; - /** - * The date/time for the end of the timespan. - */ - absoluteEndDate: number; /** * The color to use for the display of down states. */ @@ -44,23 +37,23 @@ export interface MonitorBarSeriesProps { * so we will only render the series component if there are down counts for the selected monitor. * @param props - the values for the monitor this chart visualizes */ -export const MonitorBarSeries = ({ - absoluteStartDate, - absoluteEndDate, - dangerColor, - histogramSeries, -}: MonitorBarSeriesProps) => { +export const MonitorBarSeries = ({ dangerColor, histogramSeries }: MonitorBarSeriesProps) => { + const [getUrlParams] = useUrlParams(); + const { absoluteDateRangeStart, absoluteDateRangeEnd } = getUrlParams(); + const id = 'downSeries'; return seriesHasDownValues(histogramSeries) ? (
- + ; -export const MonitorChartsComponent = ({ - data, - mean, - range, - monitorId, - dateRangeStart, - dateRangeEnd, - loading, -}: Props) => { - const [getUrlParams] = useUrlParams(); +export const MonitorChartsComponent = ({ data, mean, range, monitorId, loading }: Props) => { if (data && data.monitorChartsData) { const { monitorChartsData: { locationDurationLines }, } = data; - const { absoluteDateRangeStart, absoluteDateRangeEnd } = getUrlParams(); - return ( @@ -58,15 +44,7 @@ export const MonitorChartsComponent = ({ /> - + ); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap index f779efca7b18a7..3655db5aaff1ea 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap @@ -106,6 +106,560 @@ exports[`MonitorList component renders a no items message when no data is provid `; exports[`MonitorList component renders the monitor list 1`] = ` +.c1 { + padding-left: 17px; +} + +.c2 { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +@media (max-width:574px) { + .c0 { + min-width: 230px; + } +} + +
+
+ Monitor status +
+
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + Status + +
+
+
+ + Name + +
+
+
+ + Url + +
+
+
+ + Downtime history + +
+
+
+ +
+
+
+ Status +
+
+
+
+
+
+
+ +
+
+
+
+ + +
+
+ 1897 Yr ago +
+
+
+
+
+
+
+ in 0/1 Location +
+
+
+
+
+
+ Name +
+ +
+
+ Url +
+
+ +
+
+
+ +
+
+ -- +
+
+
+
+
+
+ +
+
+
+ Status +
+
+
+
+
+
+
+ +
+
+
+
+ + +
+
+ 1895 Yr ago +
+
+
+
+
+
+
+ in 1/1 Location +
+
+
+
+
+
+ Name +
+ +
+
+ Url +
+
+ +
+
+
+ +
+
+ -- +
+
+
+
+
+
+ +
+
+
+
+
+
+
+ +
+
+ +
+
+
+`; + +exports[`MonitorList component shallow renders the monitor list 1`] = ` `; -exports[`MonitorList component renders the monitor list 1`] = ` +exports[`MonitorListPagination component renders the monitor list 1`] = ` { let result: MonitorSummaryResult; @@ -81,11 +82,9 @@ describe('MonitorList component', () => { }; }); - it('renders the monitor list', () => { + it('shallow renders the monitor list', () => { const component = shallowWithIntl( { it('renders a no items message when no data is provided', () => { const component = shallowWithIntl( { ); expect(component).toMatchSnapshot(); }); + + it('renders the monitor list', () => { + const component = renderWithIntl( + renderWithRouter( + + ) + ); + + expect(component).toMatchSnapshot(); + }); }); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/monitor_list_pagination.test.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/monitor_list_pagination.test.tsx index a1725134094559..ff54e61006156b 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/monitor_list_pagination.test.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/monitor_list_pagination.test.tsx @@ -13,7 +13,7 @@ import { } from '../../../../../common/graphql/types'; import { MonitorListComponent } from '../monitor_list'; -describe('MonitorList component', () => { +describe('MonitorListPagination component', () => { let result: MonitorSummaryResult; beforeEach(() => { @@ -98,8 +98,6 @@ describe('MonitorList component', () => { it('renders the monitor list', () => { const component = shallowWithIntl( { it('renders a no items message when no data is provided', () => { const component = shallowWithIntl( { - const { - absoluteStartDate, - absoluteEndDate, - dangerColor, - data, - errors, - hasActiveFilters, - linkParameters, - loading, - } = props; + const { dangerColor, data, errors, hasActiveFilters, linkParameters, loading } = props; const [drawerIds, updateDrawerIds] = useState([]); const items = data?.monitorStates?.summaries ?? []; @@ -132,12 +121,7 @@ export const MonitorListComponent = (props: Props) => { show: false, }, render: (histogramSeries: SummaryHistogramPoint[] | null) => ( - + ), }, { diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/monitor_status.bar.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/__test__/__snapshots__/monitor_status.bar.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/monitor_status.bar.test.tsx.snap rename to x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/__test__/__snapshots__/monitor_status.bar.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/monitor_status.bar.test.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/__test__/monitor_status.bar.test.tsx similarity index 79% rename from x-pack/legacy/plugins/uptime/public/components/functional/__tests__/monitor_status.bar.test.tsx rename to x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/__test__/monitor_status.bar.test.tsx index 545405f91d5374..0a53eeb89d793f 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/monitor_status.bar.test.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/__test__/monitor_status.bar.test.tsx @@ -7,14 +7,12 @@ import moment from 'moment'; import React from 'react'; import { renderWithIntl } from 'test_utils/enzyme_helpers'; -import { Ping } from '../../../../common/graphql/types'; -import { MonitorStatusBarComponent } from '../monitor_status_details/monitor_status_bar'; +import { MonitorStatusBarComponent } from '../monitor_status_bar'; +import { Ping } from '../../../../../common/graphql/types'; describe('MonitorStatusBar component', () => { let monitorStatus: Ping; let monitorLocations: any; - let dateStart: string; - let dateEnd: string; beforeEach(() => { monitorStatus = { @@ -46,9 +44,6 @@ describe('MonitorStatusBar component', () => { }, ], }; - - dateStart = moment('01-01-2010').toString(); - dateEnd = moment('10-10-2010').toString(); }); it('renders duration in ms, not us', () => { @@ -56,10 +51,7 @@ describe('MonitorStatusBar component', () => { ); expect(component).toMatchSnapshot(); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/index.ts b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/index.ts index 7b4e1ea353c11c..385788cc825a0a 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/index.ts +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/index.ts @@ -3,34 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { connect } from 'react-redux'; -import { AppState } from '../../../state'; -import { selectMonitorLocations } from '../../../state/selectors'; -import { fetchMonitorLocations } from '../../../state/actions/monitor'; -import { MonitorStatusDetailsComponent } from './monitor_status_details'; -const mapStateToProps = (state: AppState, { monitorId }: any) => ({ - monitorLocations: selectMonitorLocations(state, monitorId), -}); - -const mapDispatchToProps = (dispatch: any, ownProps: any) => ({ - loadMonitorLocations: () => { - const { dateStart, dateEnd, monitorId } = ownProps; - dispatch( - fetchMonitorLocations({ - monitorId, - dateStart, - dateEnd, - }) - ); - }, -}); - -export const MonitorStatusDetails = connect( - mapStateToProps, - mapDispatchToProps -)(MonitorStatusDetailsComponent); - -export * from './monitor_status_details'; -export { MonitorStatusBar } from './monitor_status_bar'; +export { MonitorStatusBarComponent } from './monitor_status_bar'; +export { MonitorStatusDetailsComponent } from './monitor_status_details'; export { StatusByLocations } from './monitor_status_bar/status_by_location'; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_bar/index.ts b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_bar/index.ts index 94bd7fa7f026bc..0cb11587eee480 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_bar/index.ts +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_bar/index.ts @@ -4,47 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -import { connect } from 'react-redux'; -import { Dispatch } from 'redux'; -import { - StateProps, - DispatchProps, - MonitorStatusBarComponent, - MonitorStatusBarProps, -} from './monitor_status_bar'; -import { selectMonitorStatus, selectMonitorLocations } from '../../../../state/selectors'; -import { AppState } from '../../../../state'; -import { getMonitorStatus, getSelectedMonitor } from '../../../../state/actions'; - -const mapStateToProps = (state: AppState, ownProps: MonitorStatusBarProps) => ({ - monitorStatus: selectMonitorStatus(state), - monitorLocations: selectMonitorLocations(state, ownProps.monitorId), -}); - -const mapDispatchToProps = (dispatch: Dispatch, ownProps: MonitorStatusBarProps) => ({ - loadMonitorStatus: () => { - const { dateStart, dateEnd, monitorId } = ownProps; - dispatch( - getMonitorStatus({ - monitorId, - dateStart, - dateEnd, - }) - ); - dispatch( - getSelectedMonitor({ - monitorId, - }) - ); - }, -}); - -// @ts-ignore TODO: Investigate typescript issues here -export const MonitorStatusBar = connect( - // @ts-ignore TODO: Investigate typescript issues here - mapStateToProps, - mapDispatchToProps -)(MonitorStatusBarComponent); - export { MonitorSSLCertificate } from './monitor_ssl_certificate'; -export * from './monitor_status_bar'; +export { MonitorStatusBarComponent } from './monitor_status_bar'; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_bar/monitor_status_bar.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_bar/monitor_status_bar.tsx index 2524039829add1..22e4377944be13 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_bar/monitor_status_bar.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_bar/monitor_status_bar.tsx @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import React from 'react'; import { EuiLink, EuiTitle, @@ -13,42 +14,23 @@ import { EuiFlexGroup, EuiFlexItem, } from '@elastic/eui'; -import React, { useEffect } from 'react'; import { MonitorSSLCertificate } from './monitor_ssl_certificate'; import * as labels from './translations'; import { StatusByLocations } from './status_by_location'; import { Ping } from '../../../../../common/graphql/types'; import { MonitorLocations } from '../../../../../common/runtime_types'; -export interface StateProps { +interface MonitorStatusBarProps { + monitorId: string; monitorStatus: Ping; monitorLocations: MonitorLocations; } -export interface DispatchProps { - loadMonitorStatus: () => void; -} - -export interface MonitorStatusBarProps { - monitorId: string; - dateStart: string; - dateEnd: string; -} - -type Props = MonitorStatusBarProps & StateProps & DispatchProps; - -export const MonitorStatusBarComponent: React.FC = ({ - dateStart, - dateEnd, +export const MonitorStatusBarComponent: React.FC = ({ monitorId, - loadMonitorStatus, monitorStatus, monitorLocations, }) => { - useEffect(() => { - loadMonitorStatus(); - }, [dateStart, dateEnd, loadMonitorStatus]); - const full = monitorStatus?.url?.full ?? ''; return ( diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_details.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_details.tsx index 29bd8eb3a71837..7dea73da7bba0e 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_details.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_details.tsx @@ -8,16 +8,13 @@ import React, { useContext, useEffect, useState } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; import styled from 'styled-components'; import { LocationMap } from '../location_map'; -import { MonitorStatusBar } from './monitor_status_bar'; import { UptimeRefreshContext } from '../../../contexts'; +import { MonitorLocations } from '../../../../common/runtime_types'; +import { MonitorStatusBar } from '../../connected'; -interface MonitorStatusBarProps { +interface MonitorStatusDetailsProps { monitorId: string; - variables: any; - loadMonitorLocations: any; - monitorLocations: any; - dateStart: any; - dateEnd: any; + monitorLocations: MonitorLocations; } const WrapFlexItem = styled(EuiFlexItem)` @@ -28,15 +25,8 @@ const WrapFlexItem = styled(EuiFlexItem)` export const MonitorStatusDetailsComponent = ({ monitorId, - variables, - loadMonitorLocations, monitorLocations, - dateStart, - dateEnd, -}: MonitorStatusBarProps) => { - useEffect(() => { - loadMonitorLocations(monitorId); - }, [loadMonitorLocations, monitorId, dateStart, dateEnd]); +}: MonitorStatusDetailsProps) => { const { refreshApp } = useContext(UptimeRefreshContext); const [isTabActive] = useState(document.visibilityState); @@ -63,12 +53,7 @@ export const MonitorStatusDetailsComponent = ({ - + diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/snapshot.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/snapshot.tsx index 90d716001cff96..8531cd1a3cc83d 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/snapshot.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/snapshot.tsx @@ -5,63 +5,28 @@ */ import { EuiSpacer } from '@elastic/eui'; -import React, { useEffect } from 'react'; +import React from 'react'; import { get } from 'lodash'; -import { connect } from 'react-redux'; -import { Snapshot as SnapshotType } from '../../../common/runtime_types'; import { DonutChart } from './charts'; -import { fetchSnapshotCount } from '../../state/actions'; import { ChartWrapper } from './charts/chart_wrapper'; import { SnapshotHeading } from './snapshot_heading'; -import { AppState } from '../../state'; +import { Snapshot as SnapshotType } from '../../../common/runtime_types'; const SNAPSHOT_CHART_WIDTH = 144; const SNAPSHOT_CHART_HEIGHT = 144; -/** - * Props expected from parent components. - */ -interface OwnProps { - dateRangeStart: string; - dateRangeEnd: string; - filters?: string; - /** - * Height is needed, since by default charts takes height of 100% - */ - height?: string; - statusFilter?: string; -} - -/** - * Props given by the Redux store based on action input. - */ -interface StoreProps { +interface SnapshotComponentProps { count: SnapshotType; - lastRefresh: number; loading: boolean; + height?: string; } /** - * Contains functions that will dispatch actions used - * for this component's life cycle - */ -interface DispatchProps { - loadSnapshotCount: typeof fetchSnapshotCount; -} - -/** - * Props used to render the Snapshot component. + * This component visualizes a KPI and histogram chart to help users quickly + * glean the status of their uptime environment. + * @param props the props required by the component */ -type Props = OwnProps & StoreProps & DispatchProps; - -type PresentationalComponentProps = Pick & - Pick; - -export const PresentationalComponent: React.FC = ({ - count, - height, - loading, -}) => ( +export const SnapshotComponent: React.FC = ({ count, height, loading }) => ( (count, 'down', 0)} total={get(count, 'total', 0)} /> @@ -73,59 +38,3 @@ export const PresentationalComponent: React.FC = ( /> ); - -/** - * This component visualizes a KPI and histogram chart to help users quickly - * glean the status of their uptime environment. - * @param props the props required by the component - */ -export const Container: React.FC = ({ - count, - dateRangeStart, - dateRangeEnd, - filters, - height, - statusFilter, - lastRefresh, - loading, - loadSnapshotCount, -}: Props) => { - useEffect(() => { - loadSnapshotCount(dateRangeStart, dateRangeEnd, filters, statusFilter); - }, [dateRangeStart, dateRangeEnd, filters, lastRefresh, loadSnapshotCount, statusFilter]); - return ; -}; - -/** - * Provides state to connected component. - * @param state the root app state - */ -const mapStateToProps = ({ - snapshot: { count, loading }, - ui: { lastRefresh }, -}: AppState): StoreProps => ({ - count, - lastRefresh, - loading, -}); - -/** - * Used for fetching snapshot counts. - * @param dispatch redux-provided action dispatcher - */ -const mapDispatchToProps = (dispatch: any) => ({ - loadSnapshotCount: ( - dateRangeStart: string, - dateRangeEnd: string, - filters?: string, - statusFilter?: string - ): DispatchProps => { - return dispatch(fetchSnapshotCount(dateRangeStart, dateRangeEnd, filters, statusFilter)); - }, -}); - -export const Snapshot = connect( - // @ts-ignore connect is expecting null | undefined for some reason - mapStateToProps, - mapDispatchToProps -)(Container); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/status_panel.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/status_panel.tsx index 03ab9fb5cf194b..2c0be2aa15d6fb 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/status_panel.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/status_panel.tsx @@ -4,52 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; import React from 'react'; -import { Snapshot } from './snapshot'; -import { PingHistogram } from '../connected'; - -interface StatusPanelProps { - absoluteDateRangeStart: number; - absoluteDateRangeEnd: number; - dateRangeStart: string; - dateRangeEnd: string; - filters?: string; - statusFilter?: string; -} +import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; +import { PingHistogram, Snapshot } from '../connected'; const STATUS_CHART_HEIGHT = '160px'; -export const StatusPanel = ({ - absoluteDateRangeStart, - absoluteDateRangeEnd, - dateRangeStart, - dateRangeEnd, - filters, - statusFilter, -}: StatusPanelProps) => ( +export const StatusPanel = ({}) => ( - + - + diff --git a/x-pack/legacy/plugins/uptime/public/hooks/update_kuery_string.ts b/x-pack/legacy/plugins/uptime/public/hooks/update_kuery_string.ts index d02a6fc2afb5d2..8c9eec3abe2239 100644 --- a/x-pack/legacy/plugins/uptime/public/hooks/update_kuery_string.ts +++ b/x-pack/legacy/plugins/uptime/public/hooks/update_kuery_string.ts @@ -55,9 +55,9 @@ export const useUpdateKueryString = ( const elasticsearchQuery = esKuery.toElasticsearchQuery(ast, indexPattern); esFilters = JSON.stringify(elasticsearchQuery); - - updateEsQueryForFilterGroup(filterQueryString, indexPattern); } + updateEsQueryForFilterGroup(filterQueryString, indexPattern); + return [esFilters]; } catch (err) { return [urlFilters, err]; diff --git a/x-pack/legacy/plugins/uptime/public/pages/monitor.tsx b/x-pack/legacy/plugins/uptime/public/pages/monitor.tsx index 408d2584911e0d..a8501ff14313a8 100644 --- a/x-pack/legacy/plugins/uptime/public/pages/monitor.tsx +++ b/x-pack/legacy/plugins/uptime/public/pages/monitor.tsx @@ -12,8 +12,8 @@ import { UMUpdateBreadcrumbs } from '../lib/lib'; import { UptimeRefreshContext, UptimeThemeContext } from '../contexts'; import { useUptimeTelemetry, useUrlParams, UptimePage } from '../hooks'; import { useTrackPageview } from '../../../infra/public'; -import { MonitorStatusDetails } from '../components/functional/monitor_status_details'; import { PageHeader } from './page_header'; +import { MonitorStatusDetails } from '../components/connected'; interface MonitorPageProps { setBreadcrumbs: UMUpdateBreadcrumbs; @@ -49,20 +49,9 @@ export const MonitorPage = ({ setBreadcrumbs }: MonitorPageProps) => { - + - + } - +