From 0b644117b845984afeaea20ee5681dac358d5a0e Mon Sep 17 00:00:00 2001 From: Daniil Suleiman <31325372+sulemanof@users.noreply.github.com> Date: Thu, 20 Feb 2020 13:40:34 +0300 Subject: [PATCH 1/9] [Visualize] Remove legacy appState in visualize (#57330) * Remove legacy appState in visualize * Read query and linked prop from scope * Fix persisted state instance * Fix functional tests * Bound url updates with an editor * Some improvements * Fix comments --- .../components/editor/controls_tab.test.tsx | 5 +- .../kibana/public/visualize/legacy_imports.ts | 3 - .../public/visualize/np_ready/application.ts | 8 - .../visualize/np_ready/editor/editor.html | 11 +- .../visualize/np_ready/editor/editor.js | 218 +++++++++--------- .../editor/lib/{index.js => index.ts} | 3 +- .../np_ready/editor/lib/make_stateful.ts | 58 +++++ ...rate_app_state.js => migrate_app_state.ts} | 47 ++-- .../editor/lib/visualize_app_state.ts | 112 +++++++++ .../np_ready/editor/visualization.js | 5 +- .../np_ready/editor/visualization_editor.js | 5 +- .../public/visualize/np_ready/types.d.ts | 31 ++- .../public/components/sidebar/sidebar.tsx | 17 +- .../public/components/vis_editor.js | 1 - .../public/embeddable/visualize_embeddable.ts | 8 +- .../public/legacy/build_pipeline.test.ts | 10 +- .../np_ready/public/legacy/build_pipeline.ts | 4 +- .../public/np_ready/public/vis.ts | 8 + .../public/np_ready/public/vis_impl.d.ts | 10 +- .../apps/visualize/_heatmap_chart.js | 4 +- 20 files changed, 400 insertions(+), 168 deletions(-) rename src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/{index.js => index.ts} (87%) create mode 100644 src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/make_stateful.ts rename src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/{migrate_app_state.js => migrate_app_state.ts} (61%) create mode 100644 src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/visualize_app_state.ts diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/controls_tab.test.tsx b/src/legacy/core_plugins/input_control_vis/public/components/editor/controls_tab.test.tsx index 3419d773bd09e6..d7a62e07b26f31 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/controls_tab.test.tsx +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/controls_tab.test.tsx @@ -23,6 +23,7 @@ import { shallowWithIntl, mountWithIntl } from 'test_utils/enzyme_helpers'; import { findTestSubject } from '@elastic/eui/lib/test'; import { getDepsMock, getIndexPatternMock } from '../../test_utils'; import { ControlsTab, ControlsTabUiProps } from './controls_tab'; +import { Vis } from 'src/legacy/core_plugins/visualizations/public'; const indexPatternsMock = { get: getIndexPatternMock, @@ -32,7 +33,7 @@ let props: ControlsTabUiProps; beforeEach(() => { props = { deps: getDepsMock(), - vis: { + vis: ({ API: { indexPatterns: indexPatternsMock, }, @@ -46,7 +47,7 @@ beforeEach(() => { requiresSearch: false, hidden: false, }, - }, + } as unknown) as Vis, stateParams: { controls: [ { diff --git a/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts b/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts index 92433799ba4204..8b1bb0fda8c841 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts @@ -24,14 +24,11 @@ * directly where they are needed. */ -// @ts-ignore -export { AppState, AppStateProvider } from 'ui/state_management/app_state'; export { State } from 'ui/state_management/state'; // @ts-ignore export { GlobalStateProvider } from 'ui/state_management/global_state'; // @ts-ignore export { StateManagementConfigProvider } from 'ui/state_management/config_provider'; -export { stateMonitorFactory } from 'ui/state_management/state_monitor_factory'; export { PersistedState } from 'ui/persisted_state'; export { subscribeWithScope } from 'ui/utils/subscribe_with_scope'; diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/application.ts b/src/legacy/core_plugins/kibana/public/visualize/np_ready/application.ts index bd7b478f827a65..6a8d9ce106f9dc 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/application.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/application.ts @@ -22,8 +22,6 @@ import { i18nDirective, i18nFilter, I18nProvider } from '@kbn/i18n/angular'; import { AppMountContext } from 'kibana/public'; import { - AppStateProvider, - AppState, configureAppAngularModule, createTopNavDirective, createTopNavHelper, @@ -116,12 +114,6 @@ function createLocalStateModule() { 'app/visualize/Promise', 'app/visualize/PersistedState', ]) - .factory('AppState', function(Private: IPrivate) { - return Private(AppStateProvider); - }) - .service('getAppState', function(Private: IPrivate) { - return Private(AppStateProvider).getAppState; - }) .service('globalState', function(Private: IPrivate) { return Private(GlobalStateProvider); }); diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.html b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.html index 6190b92c9be3e1..4979d9dc89a0ce 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.html +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.html @@ -2,7 +2,7 @@
@@ -42,9 +42,9 @@ show-filter-bar="showFilterBar() && isVisible" show-date-picker="showQueryBarTimePicker()" show-auto-refresh-only="!showQueryBarTimePicker()" - query="state.query" + query="query" saved-query="savedQuery" - screen-title="state.vis.title" + screen-title="vis.title" on-query-submit="updateQueryAndFetch" index-patterns="[indexPattern]" filters="filters" @@ -97,7 +97,9 @@ ui-state="uiState" time-range="timeRange" filters="filters" - query="query"/> + query="query" + app-state="appState" + />

diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js index 409d4b41fbe696..415949f88e9d17 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js @@ -24,7 +24,7 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { migrateAppState } from './lib'; +import { makeStateful, useVisualizeAppState } from './lib'; import { VisualizeConstants } from '../visualize_constants'; import { getEditBreadcrumbs } from '../breadcrumbs'; @@ -45,7 +45,6 @@ import { absoluteToParsedUrl, KibanaParsedUrl, migrateLegacyQuery, - stateMonitorFactory, DashboardConstants, } from '../../legacy_imports'; @@ -68,15 +67,14 @@ function VisualizeAppController( $scope, $element, $route, - AppState, $window, $injector, $timeout, kbnUrl, redirectWhenMissing, Promise, - getAppState, - globalState + globalState, + config ) { const { indexPatterns, @@ -99,7 +97,6 @@ function VisualizeAppController( setActiveUrl, } = getServices(); - const filterStateManager = new FilterStateManager(globalState, getAppState, filterManager); // Retrieve the resolved SavedVis instance. const savedVis = $route.current.locals.savedVis; const _applyVis = () => { @@ -113,9 +110,9 @@ function VisualizeAppController( $scope.vis = vis; - const $appStatus = (this.appStatus = { + const $appStatus = { dirty: !savedVis.id, - }); + }; vis.on('dirtyStateChange', ({ isDirty }) => { vis.dirty = isDirty; @@ -265,53 +262,61 @@ function VisualizeAppController( }, ]; - let stateMonitor; - if (savedVis.id) { chrome.docTitle.change(savedVis.title); } + const defaultQuery = { + query: '', + language: + localStorage.get('kibana.userQueryLanguage') || uiSettings.get('search:queryLanguage'), + }; + // Extract visualization state with filtered aggs. You can see these filtered aggs in the URL. // Consists of things like aggs, params, listeners, title, type, etc. const savedVisState = vis.getState(); const stateDefaults = { uiState: savedVis.uiStateJSON ? JSON.parse(savedVis.uiStateJSON) : {}, - linked: !!savedVis.savedSearchId, - query: searchSource.getOwnField('query') || { - query: '', - language: - localStorage.get('kibana.userQueryLanguage') || uiSettings.get('search:queryLanguage'), - }, + query: searchSource.getOwnField('query') || defaultQuery, filters: searchSource.getOwnField('filter') || [], vis: savedVisState, + linked: !!savedVis.savedSearchId, }; - // Instance of app_state.js. - const $state = (function initState() { - // This is used to sync visualization state with the url when `appState.save()` is called. - const appState = new AppState(stateDefaults); - - // Initializing appState does two things - first it translates the defaults into AppState, - // second it updates appState based on the url (the url trumps the defaults). This means if - // we update the state format at all and want to handle BWC, we must not only migrate the - // data stored with saved vis, but also any old state in the url. - migrateAppState(appState); - - // The savedVis is pulled from elasticsearch, but the appState is pulled from the url, with the - // defaults applied. If the url was from a previous session which included modifications to the - // appState then they won't be equal. - if (!angular.equals(appState.vis, savedVisState)) { - Promise.try(function() { - vis.setState(appState.vis); - }).catch( - redirectWhenMissing({ - 'index-pattern-field': '/visualize', - }) - ); - } + const useHash = config.get('state:storeInSessionStorage'); + const { stateContainer, stopStateSync } = useVisualizeAppState({ + useHash, + stateDefaults, + }); + + const filterStateManager = new FilterStateManager( + globalState, + () => { + // Temporary AppState replacement + return { + set filters(_filters) { + stateContainer.transitions.set('filters', _filters); + }, + get filters() { + return stateContainer.getState().filters; + }, + }; + }, + filterManager + ); - return appState; - })(); + // The savedVis is pulled from elasticsearch, but the appState is pulled from the url, with the + // defaults applied. If the url was from a previous session which included modifications to the + // appState then they won't be equal. + if (!_.isEqual(stateContainer.getState().vis, stateDefaults.vis)) { + try { + vis.setState(stateContainer.getState().vis); + } catch { + redirectWhenMissing({ + 'index-pattern-field': '/visualize', + }); + } + } $scope.filters = filterManager.getFilters(); @@ -330,8 +335,6 @@ function VisualizeAppController( ); function init() { - // export some objects - $scope.savedVis = savedVis; if (vis.indexPattern) { $scope.indexPattern = vis.indexPattern; } else { @@ -340,14 +343,28 @@ function VisualizeAppController( }); } + const initialState = stateContainer.getState(); + + $scope.appState = { + // mock implementation of the legacy appState.save() + // this could be even replaced by passing only "updateAppState" callback + save() { + stateContainer.transitions.updateVisState(vis.getState()); + }, + }; + + // Create a PersistedState instance for uiState. + const { persistedState, unsubscribePersisted, persistOnChange } = makeStateful( + 'uiState', + stateContainer + ); + $scope.uiState = persistedState; + $scope.savedVis = savedVis; + $scope.query = initialState.query; + $scope.linked = initialState.linked; $scope.searchSource = searchSource; - $scope.state = $state; $scope.refreshInterval = timefilter.getRefreshInterval(); - // Create a PersistedState instance. - $scope.uiState = $state.makeStateful('uiState'); - $scope.appStatus = $appStatus; - const addToDashMode = $route.current.params[DashboardConstants.ADD_VISUALIZATION_TO_DASHBOARD_MODE_PARAM]; kbnUrl.removeParam(DashboardConstants.ADD_VISUALIZATION_TO_DASHBOARD_MODE_PARAM); @@ -372,22 +389,23 @@ function VisualizeAppController( $scope.timeRange = timefilter.getTime(); $scope.opts = _.pick($scope, 'savedVis', 'isAddToDashMode'); - stateMonitor = stateMonitorFactory.create($state, stateDefaults); - stateMonitor.ignoreProps(['vis.listeners']).onChange(status => { - $appStatus.dirty = status.dirty || !savedVis.id; - }); + const unsubscribeStateUpdates = stateContainer.subscribe(state => { + const newQuery = migrateLegacyQuery(state.query); + if (!_.isEqual(state.query, newQuery)) { + stateContainer.transitions.set('query', newQuery); + } + persistOnChange(state); - $scope.$watch('state.query', (newQuery, oldQuery) => { - if (!_.isEqual(newQuery, oldQuery)) { - const query = migrateLegacyQuery(newQuery); - if (!_.isEqual(query, newQuery)) { - $state.query = query; - } - $scope.fetch(); + // if the browser history was changed manually we need to reflect changes in the editor + if (!_.isEqual(vis.getState(), state.vis)) { + vis.setState(state.vis); + vis.forceReload(); + vis.emit('updateEditor'); } - }); - $state.replace(); + $appStatus.dirty = true; + $scope.fetch(); + }); const updateTimeRange = () => { $scope.timeRange = timefilter.getTime(); @@ -419,10 +437,11 @@ function VisualizeAppController( // update the searchSource when query updates $scope.fetch = function() { - $state.save(); - $scope.query = $state.query; - savedVis.searchSource.setField('query', $state.query); - savedVis.searchSource.setField('filter', $state.filters); + const { query, filters, linked } = stateContainer.getState(); + $scope.query = query; + $scope.linked = linked; + savedVis.searchSource.setField('query', query); + savedVis.searchSource.setField('filter', filters); $scope.$broadcast('render'); }; @@ -446,10 +465,13 @@ function VisualizeAppController( $scope._handler.destroy(); } savedVis.destroy(); - stateMonitor.destroy(); filterStateManager.destroy(); subscriptions.unsubscribe(); $scope.vis.off('apply', _applyVis); + + unsubscribePersisted(); + unsubscribeStateUpdates(); + stopStateSync(); }); $timeout(() => { @@ -459,10 +481,10 @@ function VisualizeAppController( $scope.updateQueryAndFetch = function({ query, dateRange }) { const isUpdate = - (query && !_.isEqual(query, $state.query)) || + (query && !_.isEqual(query, stateContainer.getState().query)) || (dateRange && !_.isEqual(dateRange, $scope.timeRange)); - $state.query = query; + stateContainer.transitions.set('query', query); timefilter.setTime(dateRange); // If nothing has changed, trigger the fetch manually, otherwise it will happen as a result of the changes @@ -488,20 +510,13 @@ function VisualizeAppController( $scope.onClearSavedQuery = () => { delete $scope.savedQuery; - delete $state.savedQuery; - $state.query = { - query: '', - language: - localStorage.get('kibana.userQueryLanguage') || uiSettings.get('search:queryLanguage'), - }; + stateContainer.transitions.removeSavedQuery(defaultQuery); filterManager.setFilters(filterManager.getGlobalFilters()); - $state.save(); $scope.fetch(); }; const updateStateFromSavedQuery = savedQuery => { - $state.query = savedQuery.attributes.query; - $state.save(); + stateContainer.transitions.set('query', savedQuery.attributes.query); const savedQueryFilters = savedQuery.attributes.filters || []; const globalFilters = filterManager.getGlobalFilters(); @@ -520,44 +535,38 @@ function VisualizeAppController( $scope.fetch(); }; + // update the query if savedQuery is stored + if (stateContainer.getState().savedQuery) { + savedQueryService.getSavedQuery(stateContainer.getState().savedQuery).then(savedQuery => { + $scope.$evalAsync(() => { + $scope.savedQuery = savedQuery; + }); + }); + } + $scope.$watch('savedQuery', newSavedQuery => { if (!newSavedQuery) return; - $state.savedQuery = newSavedQuery.id; - $state.save(); + stateContainer.transitions.set('savedQuery', newSavedQuery.id); updateStateFromSavedQuery(newSavedQuery); }); - $scope.$watch('state.savedQuery', newSavedQueryId => { - if (!newSavedQueryId) { - $scope.savedQuery = undefined; - return; - } - if (!$scope.savedQuery || newSavedQueryId !== $scope.savedQuery.id) { - savedQueryService.getSavedQuery(newSavedQueryId).then(savedQuery => { - $scope.$evalAsync(() => { - $scope.savedQuery = savedQuery; - updateStateFromSavedQuery(savedQuery); - }); - }); - } - }); - /** * Called when the user clicks "Save" button. */ function doSave(saveOptions) { // vis.title was not bound and it's needed to reflect title into visState - $state.vis.title = savedVis.title; - $state.vis.type = savedVis.type || $state.vis.type; - savedVis.visState = $state.vis; + stateContainer.transitions.setVis({ + title: savedVis.title, + type: savedVis.type || stateContainer.getState().vis.type, + }); + savedVis.visState = stateContainer.getState().vis; savedVis.uiStateJSON = angular.toJson($scope.uiState.getChanges()); + $appStatus.dirty = false; return savedVis.save(saveOptions).then( function(id) { $scope.$evalAsync(() => { - stateMonitor.setInitialState($state.toJSON()); - if (id) { toastNotifications.addSuccess({ title: i18n.translate( @@ -601,8 +610,6 @@ function VisualizeAppController( chrome.setBreadcrumbs($injector.invoke(getEditBreadcrumbs)); savedVis.vis.title = savedVis.title; savedVis.vis.description = savedVis.description; - // it's needed to save the state to update url string - $state.save(); } else { kbnUrl.change(`${VisualizeConstants.EDIT_PATH}/{{id}}`, { id: savedVis.id }); } @@ -632,9 +639,8 @@ function VisualizeAppController( } $scope.unlink = function() { - if (!$state.linked) return; + if (!$scope.linked) return; - $state.linked = false; const searchSourceParent = searchSource.getParent(); const searchSourceGrandparent = searchSourceParent.getParent(); @@ -645,8 +651,10 @@ function VisualizeAppController( _.union(searchSource.getOwnField('filter'), searchSourceParent.getOwnField('filter')) ); - $state.query = searchSourceParent.getField('query'); - $state.filters = searchSourceParent.getField('filter'); + stateContainer.transitions.unlinkSavedSearch( + searchSourceParent.getField('query'), + searchSourceParent.getField('filter') + ); searchSource.setField('index', searchSourceParent.getField('index')); searchSource.setParent(searchSourceGrandparent); diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/index.js b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/index.ts similarity index 87% rename from src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/index.js rename to src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/index.ts index 42284c9a03dcd0..fa5b91b00edaf1 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/index.js +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/index.ts @@ -17,4 +17,5 @@ * under the License. */ -export { migrateAppState } from './migrate_app_state'; +export { useVisualizeAppState } from './visualize_app_state'; +export { makeStateful } from './make_stateful'; diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/make_stateful.ts b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/make_stateful.ts new file mode 100644 index 00000000000000..137d4de1fe9a82 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/make_stateful.ts @@ -0,0 +1,58 @@ +/* + * 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 { PersistedState } from '../../../legacy_imports'; +import { ReduxLikeStateContainer } from '../../../../../../../../plugins/kibana_utils/public'; +import { VisualizeAppState, VisualizeAppStateTransitions } from '../../types'; + +/** + * @returns Create a PersistedState instance, initialize state changes subscriber/unsubscriber + */ +export function makeStateful( + prop: keyof VisualizeAppState, + stateContainer: ReduxLikeStateContainer +) { + // set up the persistedState state + const persistedState = new PersistedState(); + + // update the appState when the stateful instance changes + const updateOnChange = function() { + stateContainer.transitions.set(prop, persistedState.getChanges()); + }; + + const handlerOnChange = (method: 'on' | 'off') => + persistedState[method]('change', updateOnChange); + + handlerOnChange('on'); + const unsubscribePersisted = () => handlerOnChange('off'); + + // update the stateful object when the app state changes + const persistOnChange = function(state: VisualizeAppState) { + if (state[prop]) { + persistedState.set(state[prop]); + } + }; + + const appState = stateContainer.getState(); + + // if the thing we're making stateful has an appState value, write to persisted state + if (appState[prop]) persistedState.setSilent(appState[prop]); + + return { persistedState, unsubscribePersisted, persistOnChange }; +} diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/migrate_app_state.js b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/migrate_app_state.ts similarity index 61% rename from src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/migrate_app_state.js rename to src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/migrate_app_state.ts index 049ce048239db8..7e09aece52e096 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/migrate_app_state.js +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/migrate_app_state.ts @@ -18,6 +18,7 @@ */ import { get, omit } from 'lodash'; +import { VisualizeAppState } from '../../types'; /** * Creates a new instance of AppState based on the table vis state. @@ -25,37 +26,41 @@ import { get, omit } from 'lodash'; * Dashboards have a similar implementation; see * core_plugins/kibana/public/dashboard/lib/migrate_app_state * - * @param appState {AppState} AppState class to instantiate + * @param appState {VisualizeAppState} */ -export function migrateAppState(appState) { +export function migrateAppState(appState: VisualizeAppState) { // For BWC in pre 7.0 versions where table visualizations could have multiple aggs // with `schema === 'split'`. This ensures that bookmarked URLs with deprecated params // are rewritten to the correct state. See core_plugins/table_vis/migrations. if (appState.vis.type !== 'table') { - return; + return appState; } - const visAggs = get(appState, 'vis.aggs', []); - let splitCount = 0; - const migratedAggs = visAggs.map(agg => { - if (agg.schema !== 'split') { + const visAggs: any = get(appState, 'vis.aggs'); + + if (visAggs) { + let splitCount = 0; + const migratedAggs = visAggs.map((agg: any) => { + if (agg.schema !== 'split') { + return agg; + } + + splitCount++; + if (splitCount === 1) { + return agg; // leave the first split agg unchanged + } + agg.schema = 'bucket'; + // the `row` param is exclusively used by split aggs, so we remove it + agg.params = omit(agg.params, ['row']); return agg; - } + }); - splitCount++; - if (splitCount === 1) { - return agg; // leave the first split agg unchanged + if (splitCount <= 1) { + return appState; // do nothing; we only want to touch tables with multiple split aggs } - agg.schema = 'bucket'; - // the `row` param is exclusively used by split aggs, so we remove it - agg.params = omit(agg.params, ['row']); - return agg; - }); - - if (splitCount <= 1) { - return; // do nothing; we only want to touch tables with multiple split aggs + + appState.vis.aggs = migratedAggs; } - appState.vis.aggs = migratedAggs; - appState.save(); + return appState; } diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/visualize_app_state.ts b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/visualize_app_state.ts new file mode 100644 index 00000000000000..d8de81193d857f --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/visualize_app_state.ts @@ -0,0 +1,112 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { createHashHistory } from 'history'; +import { isFunction, omit } from 'lodash'; + +import { migrateAppState } from './migrate_app_state'; +import { + createKbnUrlStateStorage, + createStateContainer, + syncState, +} from '../../../../../../../../plugins/kibana_utils/public'; +import { PureVisState, VisualizeAppState, VisualizeAppStateTransitions } from '../../types'; + +const STATE_STORAGE_KEY = '_a'; + +interface Arguments { + useHash: boolean; + stateDefaults: VisualizeAppState; +} + +function toObject(state: PureVisState): PureVisState { + return omit(state, (value, key: string) => { + return key.charAt(0) === '$' || key.charAt(0) === '_' || isFunction(value); + }); +} + +export function useVisualizeAppState({ useHash, stateDefaults }: Arguments) { + const history = createHashHistory(); + const kbnUrlStateStorage = createKbnUrlStateStorage({ + useHash, + history, + }); + const urlState = kbnUrlStateStorage.get(STATE_STORAGE_KEY); + const initialState = migrateAppState({ + ...stateDefaults, + ...urlState, + }); + + /* + make sure url ('_a') matches initial state + Initializing appState does two things - first it translates the defaults into AppState, + second it updates appState based on the url (the url trumps the defaults). This means if + we update the state format at all and want to handle BWC, we must not only migrate the + data stored with saved vis, but also any old state in the url. + */ + kbnUrlStateStorage.set(STATE_STORAGE_KEY, initialState, { replace: true }); + + const stateContainer = createStateContainer( + initialState, + { + set: state => (prop, value) => ({ ...state, [prop]: value }), + setVis: state => vis => ({ + ...state, + vis: { + ...state.vis, + ...vis, + }, + }), + removeSavedQuery: state => defaultQuery => { + const { savedQuery, ...rest } = state; + + return { + ...rest, + query: defaultQuery, + }; + }, + unlinkSavedSearch: state => (query, filters) => ({ + ...state, + query, + filters, + linked: false, + }), + updateVisState: state => newVisState => ({ ...state, vis: toObject(newVisState) }), + } + ); + + const { start: startStateSync, stop: stopStateSync } = syncState({ + storageKey: STATE_STORAGE_KEY, + stateContainer: { + ...stateContainer, + set: state => { + if (state) { + // syncState utils requires to handle incoming "null" value + stateContainer.set(state); + } + }, + }, + stateStorage: kbnUrlStateStorage, + }); + + // start syncing the appState with the ('_a') url + startStateSync(); + + return { stateContainer, stopStateSync }; +} diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization.js b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization.js index 502bd6e56fb1f9..6acdb0abdd0b50 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization.js +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization.js @@ -18,7 +18,7 @@ */ export function initVisualizationDirective(app, deps) { - app.directive('visualizationEmbedded', function($timeout, getAppState) { + app.directive('visualizationEmbedded', function($timeout) { return { restrict: 'E', scope: { @@ -27,6 +27,7 @@ export function initVisualizationDirective(app, deps) { timeRange: '=', filters: '=', query: '=', + appState: '=', }, link: function($scope, element) { $scope.renderFunction = async () => { @@ -37,7 +38,7 @@ export function initVisualizationDirective(app, deps) { timeRange: $scope.timeRange, filters: $scope.filters || [], query: $scope.query, - appState: getAppState(), + appState: $scope.appState, uiState: $scope.uiState, }); $scope._handler.render(element[0]); diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization_editor.js b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization_editor.js index 8032152f88173a..c40a10115ae4eb 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization_editor.js +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization_editor.js @@ -18,7 +18,7 @@ */ export function initVisEditorDirective(app, deps) { - app.directive('visualizationEditor', function($timeout, getAppState) { + app.directive('visualizationEditor', function($timeout) { return { restrict: 'E', scope: { @@ -27,6 +27,7 @@ export function initVisEditorDirective(app, deps) { timeRange: '=', filters: '=', query: '=', + appState: '=', }, link: function($scope, element) { const Editor = $scope.savedObj.vis.type.editor; @@ -41,7 +42,7 @@ export function initVisEditorDirective(app, deps) { timeRange: $scope.timeRange, filters: $scope.filters, query: $scope.query, - appState: getAppState(), + appState: $scope.appState, }); }; diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/types.d.ts b/src/legacy/core_plugins/kibana/public/visualize/np_ready/types.d.ts index 524bc4b3196b7c..139c247aa29cc4 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/types.d.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/types.d.ts @@ -20,10 +20,37 @@ import { TimeRange, Query, Filter, DataPublicPluginStart } from 'src/plugins/data/public'; import { IEmbeddableStart } from 'src/plugins/embeddable/public'; import { LegacyCoreStart } from 'kibana/public'; -import { VisSavedObject, AppState, PersistedState } from '../legacy_imports'; +import { VisState, Vis } from 'src/legacy/core_plugins/visualizations/public'; +import { VisSavedObject, PersistedState } from '../legacy_imports'; + +export type PureVisState = ReturnType; + +export interface VisualizeAppState { + filters: Filter[]; + uiState: PersistedState; + vis: PureVisState; + query: Query; + savedQuery?: string; + linked: boolean; +} + +export interface VisualizeAppStateTransitions { + set: ( + state: VisualizeAppState + ) => ( + prop: T, + value: VisualizeAppState[T] + ) => VisualizeAppState; + setVis: (state: VisualizeAppState) => (vis: Partial) => VisualizeAppState; + removeSavedQuery: (state: VisualizeAppState) => (defaultQuery: Query) => VisualizeAppState; + unlinkSavedSearch: ( + state: VisualizeAppState + ) => (query: Query, filters: Filter[]) => VisualizeAppState; + updateVisState: (state: VisualizeAppState) => (vis: PureVisState) => VisualizeAppState; +} export interface EditorRenderProps { - appState: AppState; + appState: { save(): void }; core: LegacyCoreStart; data: DataPublicPluginStart; embeddable: IEmbeddableStart; diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/sidebar.tsx b/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/sidebar.tsx index e33e83fd19fec6..8615bcdd1bfbd5 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/sidebar.tsx +++ b/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/sidebar.tsx @@ -26,7 +26,7 @@ import { Vis } from 'src/legacy/core_plugins/visualizations/public'; import { PersistedState, AggGroupNames } from '../../legacy_imports'; import { DefaultEditorNavBar, OptionTab } from './navbar'; import { DefaultEditorControls } from './controls'; -import { setStateParamValue, useEditorReducer, useEditorFormState } from './state'; +import { setStateParamValue, useEditorReducer, useEditorFormState, discardChanges } from './state'; import { DefaultEditorAggCommonProps } from '../agg_common_props'; interface DefaultEditorSideBarProps { @@ -104,15 +104,26 @@ function DefaultEditorSideBar({ ); useEffect(() => { - vis.on('dirtyStateChange', ({ isDirty: dirty }: { isDirty: boolean }) => { + const changeHandler = ({ isDirty: dirty }: { isDirty: boolean }) => { setDirty(dirty); if (!dirty) { resetValidity(); } - }); + }; + vis.on('dirtyStateChange', changeHandler); + + return () => vis.off('dirtyStateChange', changeHandler); }, [resetValidity, vis]); + // subscribe on external vis changes using browser history, for example press back button + useEffect(() => { + const resetHandler = () => dispatch(discardChanges(vis)); + vis.on('updateEditor', resetHandler); + + return () => vis.off('updateEditor', resetHandler); + }, [dispatch, vis]); + const dataTabProps = { dispatch, formIsTouched: formState.touched, diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_editor.js b/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_editor.js index b2dd1813e6d208..0263f5b2c851c1 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_editor.js +++ b/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_editor.js @@ -82,7 +82,6 @@ export class VisEditor extends Component { // This check should be redundant, since this method should only be called when we're in editor // mode where there's also an appState passed into us. if (this.props.appState) { - this.props.appState.vis = this.props.vis.getState(); this.props.appState.save(); } }, VIS_STATE_DEBOUNCE_DELAY); 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 9e388832283faa..e7ca5ea8037016 100644 --- a/src/legacy/core_plugins/visualizations/public/embeddable/visualize_embeddable.ts +++ b/src/legacy/core_plugins/visualizations/public/embeddable/visualize_embeddable.ts @@ -23,7 +23,6 @@ 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 { AppState } from 'ui/state_management/app_state'; import { npStart } from 'ui/new_platform'; import { IExpressionLoaderParams } from 'src/plugins/expressions/public'; import { VISUALIZE_EMBEDDABLE_TYPE } from './constants'; @@ -68,7 +67,7 @@ export interface VisualizeEmbeddableConfiguration { indexPatterns?: IIndexPattern[]; editUrl: string; editable: boolean; - appState?: AppState; + appState?: { save(): void }; uiState?: PersistedState; } @@ -79,7 +78,7 @@ export interface VisualizeInput extends EmbeddableInput { vis?: { colors?: { [key: string]: string }; }; - appState?: AppState; + appState?: { save(): void }; uiState?: PersistedState; } @@ -95,7 +94,7 @@ type ExpressionLoader = InstanceType { private handler?: ExpressionLoader; private savedVisualization: VisSavedObject; - private appState: AppState | undefined; + private appState: { save(): void } | undefined; private uiState: PersistedState; private timeRange?: TimeRange; private query?: Query; @@ -389,7 +388,6 @@ export class VisualizeEmbeddable extends Embeddable { if (this.appState) { - this.appState.vis = this.savedVisualization.vis.getState(); this.appState.save(); } diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/build_pipeline.test.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/build_pipeline.test.ts index d1017de35474a9..f73dc3e19d0ef3 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/build_pipeline.test.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/build_pipeline.test.ts @@ -26,7 +26,7 @@ import { SchemaConfig, Schemas, } from './build_pipeline'; -import { Vis, VisState } from '..'; +import { Vis } from '..'; import { IAggConfig } from '../../../legacy_imports'; import { searchSourceMock } from '../../../legacy_mocks'; @@ -83,7 +83,7 @@ describe('visualize loader pipeline helpers: build pipeline', () => { }); describe('buildPipelineVisFunction', () => { - let visStateDef: VisState; + let visStateDef: ReturnType; let schemaConfig: SchemaConfig; let schemasDef: Schemas; let uiState: any; @@ -94,7 +94,7 @@ describe('visualize loader pipeline helpers: build pipeline', () => { // @ts-ignore type: 'type', params: {}, - }; + } as ReturnType; schemaConfig = { accessor: 0, @@ -349,7 +349,7 @@ describe('visualize loader pipeline helpers: build pipeline', () => { describe('buildPipeline', () => { it('calls toExpression on vis_type if it exists', async () => { - const vis: Vis = { + const vis = ({ getCurrentState: () => {}, getUiState: () => null, isHierarchical: () => false, @@ -360,7 +360,7 @@ describe('visualize loader pipeline helpers: build pipeline', () => { type: { toExpression: () => 'testing custom expressions', }, - }; + } as unknown) as Vis; const expression = await buildPipeline(vis, { searchSource: searchSourceMock }); expect(expression).toMatchSnapshot(); }); diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/build_pipeline.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/build_pipeline.ts index 04a296a888e87f..025eef834ca86b 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/build_pipeline.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/build_pipeline.ts @@ -28,7 +28,7 @@ import { isDateHistogramBucketAggConfig, createFormat, } from '../../../legacy_imports'; -import { Vis, VisParams, VisState } from '..'; +import { Vis, VisParams } from '..'; interface SchemaConfigParams { precision?: number; @@ -59,7 +59,7 @@ export interface Schemas { } type buildVisFunction = ( - visState: VisState, + visState: ReturnType, schemas: Schemas, uiState: any, meta?: { savedObjectId?: string } diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/vis.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/vis.ts index 59a8013523ef60..19375e25a9fb72 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/vis.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/vis.ts @@ -23,6 +23,14 @@ import { Status } from './legacy/update_status'; export interface Vis { type: VisType; + getCurrentState: ( + includeDisabled?: boolean + ) => { + title: string; + type: string; + params: VisParams; + aggs: Array<{ [key: string]: any }>; + }; // Since we haven't typed everything here yet, we basically "any" the rest // of that interface. This should be removed as soon as this type definition diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/vis_impl.d.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/vis_impl.d.ts index 45d65efb5dcdff..f9b7db5c02d937 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/vis_impl.d.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/vis_impl.d.ts @@ -17,7 +17,7 @@ * under the License. */ -import { Vis, VisState } from './vis'; +import { Vis, VisState, VisParams } from './vis'; import { VisType } from './types'; import { IIndexPattern } from '../../../../../../plugins/data/common'; @@ -35,6 +35,14 @@ export declare class VisImpl implements Vis { constructor(indexPattern: IIndexPattern, visState?: InitVisStateType); type: VisType; + getCurrentState: ( + includeDisabled?: boolean + ) => { + title: string; + type: string; + params: VisParams; + aggs: Array<{ [key: string]: any }>; + }; // Since we haven't typed everything here yet, we basically "any" the rest // of that interface. This should be removed as soon as this type definition diff --git a/test/functional/apps/visualize/_heatmap_chart.js b/test/functional/apps/visualize/_heatmap_chart.js index 2cea861d0f64df..cbf7ab8df6831e 100644 --- a/test/functional/apps/visualize/_heatmap_chart.js +++ b/test/functional/apps/visualize/_heatmap_chart.js @@ -96,6 +96,8 @@ export default function({ getService, getPageObjects }) { await PageObjects.visEditor.clickOptionsTab(); await PageObjects.visEditor.changeHeatmapColorNumbers(6); await PageObjects.visEditor.clickGo(); + await PageObjects.visChart.waitForVisualizationRenderingStabilized(); + const legends = await PageObjects.visChart.getLegendEntries(); const expectedLegends = [ '0 - 267', @@ -121,9 +123,9 @@ export default function({ getService, getPageObjects }) { log.debug('customize 2 last ranges'); await PageObjects.visEditor.setCustomRangeByIndex(6, '650', '720'); await PageObjects.visEditor.setCustomRangeByIndex(7, '800', '905'); + await PageObjects.visEditor.clickGo(); await PageObjects.visChart.waitForVisualizationRenderingStabilized(); - await PageObjects.visEditor.clickGo(); const legends = await PageObjects.visChart.getLegendEntries(); const expectedLegends = [ '0 - 100', From dd8eb62d9aad1e4e279fc338a242a8a39d0422b6 Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Thu, 20 Feb 2020 12:05:08 +0100 Subject: [PATCH 2/9] Test NP logging config reload on SIGHUP (#57681) * TSify reload config test and simplify logic * get rid of mutable config in tests * increase timeouts * address comments --- .../reload_logging_config/kibana.test.yml | 3 + .../kibana_log_console.test.yml | 22 ++ .../kibana_log_file.test.yml | 22 ++ .../reload_logging_config.test.js | 224 --------------- .../reload_logging_config.test.ts | 263 ++++++++++++++++++ 5 files changed, 310 insertions(+), 224 deletions(-) create mode 100644 src/cli/serve/integration_tests/__fixtures__/reload_logging_config/kibana_log_console.test.yml create mode 100644 src/cli/serve/integration_tests/__fixtures__/reload_logging_config/kibana_log_file.test.yml delete mode 100644 src/cli/serve/integration_tests/reload_logging_config.test.js create mode 100644 src/cli/serve/integration_tests/reload_logging_config.test.ts diff --git a/src/cli/serve/integration_tests/__fixtures__/reload_logging_config/kibana.test.yml b/src/cli/serve/integration_tests/__fixtures__/reload_logging_config/kibana.test.yml index 23f33940283c0d..594c2efc8adc92 100644 --- a/src/cli/serve/integration_tests/__fixtures__/reload_logging_config/kibana.test.yml +++ b/src/cli/serve/integration_tests/__fixtures__/reload_logging_config/kibana.test.yml @@ -1,4 +1,5 @@ server: + autoListen: false port: 8274 logging: json: true @@ -6,3 +7,5 @@ optimize: enabled: false plugins: initialize: false +migrations: + skip: true diff --git a/src/cli/serve/integration_tests/__fixtures__/reload_logging_config/kibana_log_console.test.yml b/src/cli/serve/integration_tests/__fixtures__/reload_logging_config/kibana_log_console.test.yml new file mode 100644 index 00000000000000..33dd4787efad92 --- /dev/null +++ b/src/cli/serve/integration_tests/__fixtures__/reload_logging_config/kibana_log_console.test.yml @@ -0,0 +1,22 @@ +server: + autoListen: false + port: 8274 +logging: + loggers: + - context: root + appenders: + - console + level: debug + appenders: + console: + kind: console + layout: + kind: json + root: + level: debug +optimize: + enabled: false +plugins: + initialize: false +migrations: + skip: true diff --git a/src/cli/serve/integration_tests/__fixtures__/reload_logging_config/kibana_log_file.test.yml b/src/cli/serve/integration_tests/__fixtures__/reload_logging_config/kibana_log_file.test.yml new file mode 100644 index 00000000000000..f5148899ff8542 --- /dev/null +++ b/src/cli/serve/integration_tests/__fixtures__/reload_logging_config/kibana_log_file.test.yml @@ -0,0 +1,22 @@ +server: + autoListen: false + port: 8274 +logging: + loggers: + - context: root + appenders: + - file + level: debug + appenders: + file: + kind: file + layout: + kind: pattern + root: + level: debug +optimize: + enabled: false +plugins: + initialize: false +migrations: + skip: true diff --git a/src/cli/serve/integration_tests/reload_logging_config.test.js b/src/cli/serve/integration_tests/reload_logging_config.test.js deleted file mode 100644 index 82d514877aff64..00000000000000 --- a/src/cli/serve/integration_tests/reload_logging_config.test.js +++ /dev/null @@ -1,224 +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 { spawn } from 'child_process'; -import fs from 'fs'; -import path from 'path'; -import os from 'os'; -import del from 'del'; - -import { safeDump } from 'js-yaml'; -import { - createMapStream, - createSplitStream, - createPromiseFromStreams, -} from '../../../legacy/utils/streams'; -import { getConfigFromFiles } from '../../../core/server/config/read_config'; - -const testConfigFile = follow('__fixtures__/reload_logging_config/kibana.test.yml'); -const kibanaPath = follow('../../../../scripts/kibana.js'); - -const second = 1000; -const minute = second * 60; - -const tempDir = path.join(os.tmpdir(), 'kbn-reload-test'); - -function follow(file) { - return path.relative(process.cwd(), path.resolve(__dirname, file)); -} - -function setLoggingJson(enabled) { - const conf = getConfigFromFiles([testConfigFile]); - conf.logging = conf.logging || {}; - conf.logging.json = enabled; - - const yaml = safeDump(conf); - - fs.writeFileSync(testConfigFile, yaml); -} - -describe('Server logging configuration', function() { - let child; - let isJson; - - beforeEach(() => { - isJson = true; - setLoggingJson(true); - - fs.mkdirSync(tempDir, { recursive: true }); - }); - - afterEach(() => { - isJson = true; - setLoggingJson(true); - - if (child !== undefined) { - child.kill(); - child = undefined; - } - - del.sync(tempDir, { force: true }); - }); - - const isWindows = /^win/.test(process.platform); - if (isWindows) { - it('SIGHUP is not a feature of Windows.', () => { - // nothing to do for Windows - }); - } else { - it( - 'should be reloadable via SIGHUP process signaling', - async function() { - expect.assertions(3); - - child = spawn( - process.execPath, - [kibanaPath, '--config', testConfigFile, '--oss', '--verbose'], - { - stdio: 'pipe', - } - ); - - let sawJson = false; - let sawNonjson = false; - - const [exitCode] = await Promise.all([ - Promise.race([ - new Promise(r => child.once('exit', r)).then(code => (code === null ? 0 : code)), - - new Promise(r => child.once('error', r)).then(err => { - throw new Error( - `error in child process while attempting to reload config. ${err.stack || - err.message || - err}` - ); - }), - ]), - - createPromiseFromStreams([ - child.stdout, - createSplitStream('\n'), - createMapStream(async line => { - if (!line) { - // skip empty lines - return; - } - - if (isJson) { - const data = JSON.parse(line); - sawJson = true; - - // We know the sighup handler will be registered before - // root.setup() is called - if (data.message.includes('setting up root')) { - isJson = false; - setLoggingJson(false); - - // Reload logging config. We give it a little bit of time to just make - // sure the process sighup handler is registered. - await new Promise(r => setTimeout(r, 100)); - child.kill('SIGHUP'); - } - } else if (line.startsWith('{')) { - // We have told Kibana to stop logging json, but it hasn't completed - // the switch yet, so we ignore before switching over. - } else { - // Kibana has successfully stopped logging json, so kill the server. - sawNonjson = true; - - child && child.kill(); - child = undefined; - } - }), - ]), - ]); - - expect(exitCode).toEqual(0); - expect(sawJson).toEqual(true); - expect(sawNonjson).toEqual(true); - }, - minute - ); - - it( - 'should recreate file handler on SIGHUP', - function(done) { - expect.hasAssertions(); - - const logPath = path.resolve(tempDir, 'kibana.log'); - const logPathArchived = path.resolve(tempDir, 'kibana_archive.log'); - - function watchFileUntil(path, matcher, timeout) { - return new Promise((resolve, reject) => { - const timeoutHandle = setTimeout(() => { - fs.unwatchFile(path); - reject(`watchFileUntil timed out for "${matcher}"`); - }, timeout); - - fs.watchFile(path, () => { - try { - const contents = fs.readFileSync(path); - - if (matcher.test(contents)) { - clearTimeout(timeoutHandle); - fs.unwatchFile(path); - resolve(contents); - } - } catch (e) { - // noop - } - }); - }); - } - - child = spawn(process.execPath, [ - kibanaPath, - '--oss', - '--config', - testConfigFile, - '--logging.dest', - logPath, - '--plugins.initialize', - 'false', - '--logging.json', - 'false', - '--verbose', - ]); - - watchFileUntil(logPath, /starting server/, 2 * minute) - .then(() => { - // once the server is running, archive the log file and issue SIGHUP - fs.renameSync(logPath, logPathArchived); - child.kill('SIGHUP'); - }) - .then(() => - watchFileUntil(logPath, /Reloaded logging configuration due to SIGHUP/, 10 * second) - ) - .then(contents => { - const lines = contents.toString().split('\n'); - // should be the first line of the new log file - expect(lines[0]).toMatch(/Reloaded logging configuration due to SIGHUP/); - child.kill(); - }) - .then(done, done); - }, - 3 * minute - ); - } -}); diff --git a/src/cli/serve/integration_tests/reload_logging_config.test.ts b/src/cli/serve/integration_tests/reload_logging_config.test.ts new file mode 100644 index 00000000000000..2def3569828d34 --- /dev/null +++ b/src/cli/serve/integration_tests/reload_logging_config.test.ts @@ -0,0 +1,263 @@ +/* + * 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 Child from 'child_process'; +import Fs from 'fs'; +import Path from 'path'; +import Os from 'os'; +import Del from 'del'; + +import * as Rx from 'rxjs'; +import { map, filter, take } from 'rxjs/operators'; +import { safeDump } from 'js-yaml'; + +import { getConfigFromFiles } from '../../../core/server/config/read_config'; + +const legacyConfig = follow('__fixtures__/reload_logging_config/kibana.test.yml'); +const configFileLogConsole = follow( + '__fixtures__/reload_logging_config/kibana_log_console.test.yml' +); +const configFileLogFile = follow('__fixtures__/reload_logging_config/kibana_log_file.test.yml'); + +const kibanaPath = follow('../../../../scripts/kibana.js'); + +const second = 1000; +const minute = second * 60; + +const tempDir = Path.join(Os.tmpdir(), 'kbn-reload-test'); + +function follow(file: string) { + return Path.relative(process.cwd(), Path.resolve(__dirname, file)); +} + +function watchFileUntil(path: string, matcher: RegExp, timeout: number) { + return new Promise((resolve, reject) => { + const timeoutHandle = setTimeout(() => { + Fs.unwatchFile(path); + reject(`watchFileUntil timed out for "${matcher}"`); + }, timeout); + + Fs.watchFile(path, () => { + try { + const contents = Fs.readFileSync(path, 'utf-8'); + + if (matcher.test(contents)) { + clearTimeout(timeoutHandle); + Fs.unwatchFile(path); + resolve(contents); + } + } catch (e) { + // noop + } + }); + }); +} + +function containsJsonOnly(content: string[]) { + return content.every(line => line.startsWith('{')); +} + +function createConfigManager(configPath: string) { + return { + modify(fn: (input: Record) => Record) { + const oldContent = getConfigFromFiles([configPath]); + const yaml = safeDump(fn(oldContent)); + Fs.writeFileSync(configPath, yaml); + }, + }; +} + +describe('Server logging configuration', function() { + let child: Child.ChildProcess; + beforeEach(() => { + Fs.mkdirSync(tempDir, { recursive: true }); + }); + + afterEach(async () => { + if (child !== undefined) { + child.kill(); + // wait for child to be killed otherwise jest complains that process not finished + await new Promise(res => setTimeout(res, 1000)); + } + Del.sync(tempDir, { force: true }); + }); + + const isWindows = /^win/.test(process.platform); + if (isWindows) { + it('SIGHUP is not a feature of Windows.', () => { + // nothing to do for Windows + }); + } else { + describe('legacy logging', () => { + it( + 'should be reloadable via SIGHUP process signaling', + async function() { + const configFilePath = Path.resolve(tempDir, 'kibana.yml'); + Fs.copyFileSync(legacyConfig, configFilePath); + + child = Child.spawn(process.execPath, [ + kibanaPath, + '--oss', + '--config', + configFilePath, + '--verbose', + ]); + + const message$ = Rx.fromEvent(child.stdout, 'data').pipe( + map(messages => + String(messages) + .split('\n') + .filter(Boolean) + ) + ); + + await message$ + .pipe( + // We know the sighup handler will be registered before this message logged + filter(messages => messages.some(m => m.includes('setting up root'))), + take(1) + ) + .toPromise(); + + const lastMessage = await message$.pipe(take(1)).toPromise(); + expect(containsJsonOnly(lastMessage)).toBe(true); + + createConfigManager(configFilePath).modify(oldConfig => { + oldConfig.logging.json = false; + return oldConfig; + }); + + child.kill('SIGHUP'); + + await message$ + .pipe( + filter(messages => !containsJsonOnly(messages)), + take(1) + ) + .toPromise(); + }, + minute + ); + + it( + 'should recreate file handle on SIGHUP', + async function() { + const logPath = Path.resolve(tempDir, 'kibana.log'); + const logPathArchived = Path.resolve(tempDir, 'kibana_archive.log'); + + child = Child.spawn(process.execPath, [ + kibanaPath, + '--oss', + '--config', + legacyConfig, + '--logging.dest', + logPath, + '--verbose', + ]); + + await watchFileUntil(logPath, /setting up root/, 30 * second); + // once the server is running, archive the log file and issue SIGHUP + Fs.renameSync(logPath, logPathArchived); + child.kill('SIGHUP'); + + await watchFileUntil( + logPath, + /Reloaded logging configuration due to SIGHUP/, + 30 * second + ); + }, + minute + ); + }); + + describe('platform logging', () => { + it( + 'should be reloadable via SIGHUP process signaling', + async function() { + const configFilePath = Path.resolve(tempDir, 'kibana.yml'); + Fs.copyFileSync(configFileLogConsole, configFilePath); + + child = Child.spawn(process.execPath, [kibanaPath, '--oss', '--config', configFilePath]); + + const message$ = Rx.fromEvent(child.stdout, 'data').pipe( + map(messages => + String(messages) + .split('\n') + .filter(Boolean) + ) + ); + + await message$ + .pipe( + // We know the sighup handler will be registered before this message logged + filter(messages => messages.some(m => m.includes('setting up root'))), + take(1) + ) + .toPromise(); + + const lastMessage = await message$.pipe(take(1)).toPromise(); + expect(containsJsonOnly(lastMessage)).toBe(true); + + createConfigManager(configFilePath).modify(oldConfig => { + oldConfig.logging.appenders.console.layout.kind = 'pattern'; + return oldConfig; + }); + child.kill('SIGHUP'); + + await message$ + .pipe( + filter(messages => !containsJsonOnly(messages)), + take(1) + ) + .toPromise(); + }, + 30 * second + ); + it( + 'should recreate file handle on SIGHUP', + async function() { + const configFilePath = Path.resolve(tempDir, 'kibana.yml'); + Fs.copyFileSync(configFileLogFile, configFilePath); + + const logPath = Path.resolve(tempDir, 'kibana.log'); + const logPathArchived = Path.resolve(tempDir, 'kibana_archive.log'); + + createConfigManager(configFilePath).modify(oldConfig => { + oldConfig.logging.appenders.file.path = logPath; + return oldConfig; + }); + + child = Child.spawn(process.execPath, [kibanaPath, '--oss', '--config', configFilePath]); + + await watchFileUntil(logPath, /setting up root/, 30 * second); + // once the server is running, archive the log file and issue SIGHUP + Fs.renameSync(logPath, logPathArchived); + child.kill('SIGHUP'); + + await watchFileUntil( + logPath, + /Reloaded logging configuration due to SIGHUP/, + 30 * second + ); + }, + minute + ); + }); + } +}); From d6aff77485ce28bc7d50a5f4bac513024c70ea57 Mon Sep 17 00:00:00 2001 From: Dmitry Lemeshko Date: Thu, 20 Feb 2020 13:12:04 +0100 Subject: [PATCH 3/9] remove NODE_ENV=test (#58037) Co-authored-by: Elastic Machine --- .ci/Jenkinsfile_coverage | 6 +----- test/scripts/jenkins_unit.sh | 1 - 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/.ci/Jenkinsfile_coverage b/.ci/Jenkinsfile_coverage index e40cc584dc376c..fa1e141be93eaa 100644 --- a/.ci/Jenkinsfile_coverage +++ b/.ci/Jenkinsfile_coverage @@ -13,11 +13,7 @@ stage("Kibana Pipeline") { // This stage is just here to help the BlueOcean UI a ]) { parallel([ 'kibana-intake-agent': { - withEnv([ - 'NODE_ENV=test' // Needed for jest tests only - ]) { - kibanaPipeline.intakeWorker('kibana-intake', './test/scripts/jenkins_unit.sh')() - } + kibanaPipeline.intakeWorker('kibana-intake', './test/scripts/jenkins_unit.sh')() }, 'x-pack-intake-agent': { withEnv([ diff --git a/test/scripts/jenkins_unit.sh b/test/scripts/jenkins_unit.sh index fe67594ad8ac2d..a9751003e84258 100755 --- a/test/scripts/jenkins_unit.sh +++ b/test/scripts/jenkins_unit.sh @@ -5,7 +5,6 @@ source test/scripts/jenkins_test_setup.sh if [[ -z "$CODE_COVERAGE" ]] ; then "$(FORCE_COLOR=0 yarn bin)/grunt" jenkins:unit --dev; else - echo "NODE_ENV=$NODE_ENV" echo " -> Running jest tests with coverage" node scripts/jest --ci --verbose --coverage echo "" From b07aa096060266e16366766d716fd3d5c7b1a30c Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Thu, 20 Feb 2020 13:25:02 +0100 Subject: [PATCH 4/9] [Watcher] Refactor to use client from `RequestHandlerContext` (#57834) * Remove elasticsearch setup service from route deps Removed the callWithRequestFactory entirely. This setup was introducing a pattern where route handlers were not pulling the ES client fromt the route handler context. For this refactor we need to extend the route handler context with watcher specific client actions and so we also extend RequestHandlerContext globally. In this commit we also update the types for params, query and body schema on each route to avoid using any everwhere. * Add generic types to license wrapper Adding to the license wrapper made it a transparent wrapper from a type perspective so we can remove the need to eplicitly set RequestHandler on the handler. Also cleaned up a variable name "response" -> "searchResults" Also removed elasticsearch from the RouteDependencies type. Co-authored-by: Elastic Machine --- x-pack/plugins/watcher/server/index.ts | 2 + .../server/lib/call_with_request_factory.ts | 28 ---- .../fetch_all_from_scroll.ts | 29 ++-- .../license_pre_routing_factory.ts | 6 +- x-pack/plugins/watcher/server/plugin.ts | 31 +++- .../routes/api/indices/register_get_route.ts | 96 ++++++------ .../api/license/register_refresh_route.ts | 10 +- .../routes/api/register_list_fields_route.ts | 64 ++++---- .../routes/api/register_load_history_route.ts | 82 +++++----- .../api/settings/register_load_route.ts | 33 ++--- .../action/register_acknowledge_route.ts | 72 +++++---- .../api/watch/register_activate_route.ts | 75 +++++----- .../api/watch/register_deactivate_route.ts | 76 +++++----- .../routes/api/watch/register_delete_route.ts | 55 ++++--- .../api/watch/register_execute_route.ts | 88 +++++------ .../api/watch/register_history_route.ts | 103 ++++++------- .../routes/api/watch/register_load_route.ts | 82 +++++----- .../routes/api/watch/register_save_route.ts | 140 +++++++++--------- .../api/watch/register_visualize_route.ts | 68 +++++---- .../api/watches/register_delete_route.ts | 42 +++--- .../routes/api/watches/register_list_route.ts | 95 ++++++------ x-pack/plugins/watcher/server/types.ts | 4 +- 22 files changed, 633 insertions(+), 648 deletions(-) delete mode 100644 x-pack/plugins/watcher/server/lib/call_with_request_factory.ts diff --git a/x-pack/plugins/watcher/server/index.ts b/x-pack/plugins/watcher/server/index.ts index 51eb7bfa543fe1..356be781fb1944 100644 --- a/x-pack/plugins/watcher/server/index.ts +++ b/x-pack/plugins/watcher/server/index.ts @@ -6,4 +6,6 @@ import { PluginInitializerContext } from 'kibana/server'; import { WatcherServerPlugin } from './plugin'; +export { WatcherContext } from './plugin'; + export const plugin = (ctx: PluginInitializerContext) => new WatcherServerPlugin(ctx); diff --git a/x-pack/plugins/watcher/server/lib/call_with_request_factory.ts b/x-pack/plugins/watcher/server/lib/call_with_request_factory.ts deleted file mode 100644 index 4884c75436c246..00000000000000 --- a/x-pack/plugins/watcher/server/lib/call_with_request_factory.ts +++ /dev/null @@ -1,28 +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 { ElasticsearchServiceSetup } from 'kibana/server'; -import { once } from 'lodash'; -import { elasticsearchJsPlugin } from './elasticsearch_js_plugin'; - -const callWithRequest = once((elasticsearchService: ElasticsearchServiceSetup) => { - const config = { plugins: [elasticsearchJsPlugin] }; - return elasticsearchService.createClient('watcher', config); -}); - -export const callWithRequestFactory = ( - elasticsearchService: ElasticsearchServiceSetup, - request: any -) => { - return (...args: any[]) => { - return ( - callWithRequest(elasticsearchService) - .asScoped(request) - // @ts-ignore - .callAsCurrentUser(...args) - ); - }; -}; diff --git a/x-pack/plugins/watcher/server/lib/fetch_all_from_scroll/fetch_all_from_scroll.ts b/x-pack/plugins/watcher/server/lib/fetch_all_from_scroll/fetch_all_from_scroll.ts index de01bd59655040..8e8ca369dd02b4 100644 --- a/x-pack/plugins/watcher/server/lib/fetch_all_from_scroll/fetch_all_from_scroll.ts +++ b/x-pack/plugins/watcher/server/lib/fetch_all_from_scroll/fetch_all_from_scroll.ts @@ -4,24 +4,31 @@ * you may not use this file except in compliance with the Elastic License. */ +import { IScopedClusterClient } from 'kibana/server'; import { get } from 'lodash'; import { ES_SCROLL_SETTINGS } from '../../../common/constants'; -export function fetchAllFromScroll(response: any, callWithRequest: any, hits: any[] = []) { - const newHits = get(response, 'hits.hits', []); - const scrollId = get(response, '_scroll_id'); +export function fetchAllFromScroll( + searchResuls: any, + dataClient: IScopedClusterClient, + hits: any[] = [] +): Promise { + const newHits = get(searchResuls, 'hits.hits', []); + const scrollId = get(searchResuls, '_scroll_id'); if (newHits.length > 0) { hits.push(...newHits); - return callWithRequest('scroll', { - body: { - scroll: ES_SCROLL_SETTINGS.KEEPALIVE, - scroll_id: scrollId, - }, - }).then((innerResponse: any) => { - return fetchAllFromScroll(innerResponse, callWithRequest, hits); - }); + return dataClient + .callAsCurrentUser('scroll', { + body: { + scroll: ES_SCROLL_SETTINGS.KEEPALIVE, + scroll_id: scrollId, + }, + }) + .then((innerResponse: any) => { + return fetchAllFromScroll(innerResponse, dataClient, hits); + }); } return Promise.resolve(hits); diff --git a/x-pack/plugins/watcher/server/lib/license_pre_routing_factory/license_pre_routing_factory.ts b/x-pack/plugins/watcher/server/lib/license_pre_routing_factory/license_pre_routing_factory.ts index d010a239527254..1b2476fc78b452 100644 --- a/x-pack/plugins/watcher/server/lib/license_pre_routing_factory/license_pre_routing_factory.ts +++ b/x-pack/plugins/watcher/server/lib/license_pre_routing_factory/license_pre_routing_factory.ts @@ -12,13 +12,13 @@ import { } from 'kibana/server'; import { RouteDependencies } from '../../types'; -export const licensePreRoutingFactory = ( +export const licensePreRoutingFactory = ( { getLicenseStatus }: RouteDependencies, - handler: RequestHandler + handler: RequestHandler ) => { return function licenseCheck( ctx: RequestHandlerContext, - request: KibanaRequest, + request: KibanaRequest, response: KibanaResponseFactory ) { const licenseStatus = getLicenseStatus(); diff --git a/x-pack/plugins/watcher/server/plugin.ts b/x-pack/plugins/watcher/server/plugin.ts index 1f7b3823609eca..51d85c2001bd28 100644 --- a/x-pack/plugins/watcher/server/plugin.ts +++ b/x-pack/plugins/watcher/server/plugin.ts @@ -3,7 +3,20 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { CoreSetup, Logger, Plugin, PluginInitializerContext } from 'kibana/server'; + +declare module 'kibana/server' { + interface RequestHandlerContext { + watcher?: WatcherContext; + } +} + +import { + CoreSetup, + IScopedClusterClient, + Logger, + Plugin, + PluginInitializerContext, +} from 'kibana/server'; import { PLUGIN } from '../common/constants'; import { Dependencies, LicenseStatus, RouteDependencies } from './types'; import { LICENSE_CHECK_STATE } from '../../licensing/server'; @@ -15,6 +28,11 @@ import { registerWatchesRoutes } from './routes/api/watches'; import { registerWatchRoutes } from './routes/api/watch'; import { registerListFieldsRoute } from './routes/api/register_list_fields_route'; import { registerLoadHistoryRoute } from './routes/api/register_load_history_route'; +import { elasticsearchJsPlugin } from './lib/elasticsearch_js_plugin'; + +export interface WatcherContext { + client: IScopedClusterClient; +} export class WatcherServerPlugin implements Plugin { log: Logger; @@ -31,15 +49,20 @@ export class WatcherServerPlugin implements Plugin { { http, elasticsearch: elasticsearchService }: CoreSetup, { licensing }: Dependencies ) { - const elasticsearch = await elasticsearchService.adminClient; const router = http.createRouter(); const routeDependencies: RouteDependencies = { - elasticsearch, - elasticsearchService, router, getLicenseStatus: () => this.licenseStatus, }; + const config = { plugins: [elasticsearchJsPlugin] }; + const watcherESClient = elasticsearchService.createClient('watcher', config); + http.registerRouteHandlerContext('watcher', (ctx, request) => { + return { + client: watcherESClient.asScoped(request), + }; + }); + registerListFieldsRoute(routeDependencies); registerLoadHistoryRoute(routeDependencies); registerIndicesRoutes(routeDependencies); diff --git a/x-pack/plugins/watcher/server/routes/api/indices/register_get_route.ts b/x-pack/plugins/watcher/server/routes/api/indices/register_get_route.ts index 30607b82e32950..df6f62135baeb6 100644 --- a/x-pack/plugins/watcher/server/routes/api/indices/register_get_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/indices/register_get_route.ts @@ -5,13 +5,14 @@ */ import { schema } from '@kbn/config-schema'; -import { RequestHandler } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { reduce, size } from 'lodash'; -import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; import { isEsError } from '../../../lib/is_es_error'; import { RouteDependencies } from '../../../types'; import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; +const bodySchema = schema.object({ pattern: schema.string() }, { allowUnknowns: true }); + function getIndexNamesFromAliasesResponse(json: Record) { return reduce( json, @@ -26,67 +27,66 @@ function getIndexNamesFromAliasesResponse(json: Record) { ); } -function getIndices(callWithRequest: any, pattern: string, limit = 10) { - return callWithRequest('indices.getAlias', { - index: pattern, - ignore: [404], - }).then((aliasResult: any) => { - if (aliasResult.status !== 404) { - const indicesFromAliasResponse = getIndexNamesFromAliasesResponse(aliasResult); - return indicesFromAliasResponse.slice(0, limit); - } - - const params = { +function getIndices(dataClient: IScopedClusterClient, pattern: string, limit = 10) { + return dataClient + .callAsCurrentUser('indices.getAlias', { index: pattern, ignore: [404], - body: { - size: 0, // no hits - aggs: { - indices: { - terms: { - field: '_index', - size: limit, + }) + .then((aliasResult: any) => { + if (aliasResult.status !== 404) { + const indicesFromAliasResponse = getIndexNamesFromAliasesResponse(aliasResult); + return indicesFromAliasResponse.slice(0, limit); + } + + const params = { + index: pattern, + ignore: [404], + body: { + size: 0, // no hits + aggs: { + indices: { + terms: { + field: '_index', + size: limit, + }, }, }, }, - }, - }; + }; - return callWithRequest('search', params).then((response: any) => { - if (response.status === 404 || !response.aggregations) { - return []; - } - return response.aggregations.indices.buckets.map((bucket: any) => bucket.key); + return dataClient.callAsCurrentUser('search', params).then((response: any) => { + if (response.status === 404 || !response.aggregations) { + return []; + } + return response.aggregations.indices.buckets.map((bucket: any) => bucket.key); + }); }); - }); } export function registerGetRoute(deps: RouteDependencies) { - const handler: RequestHandler = async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); - const { pattern } = request.body; - - try { - const indices = await getIndices(callWithRequest, pattern); - return response.ok({ body: { indices } }); - } catch (e) { - // Case: Error from Elasticsearch JS client - if (isEsError(e)) { - return response.customError({ statusCode: e.statusCode, body: e }); - } - - // Case: default - return response.internalError({ body: e }); - } - }; - deps.router.post( { path: '/api/watcher/indices', validate: { - body: schema.object({}, { allowUnknowns: true }), + body: bodySchema, }, }, - licensePreRoutingFactory(deps, handler) + licensePreRoutingFactory(deps, async (ctx, request, response) => { + const { pattern } = request.body; + + try { + const indices = await getIndices(ctx.watcher!.client, pattern); + return response.ok({ body: { indices } }); + } catch (e) { + // Case: Error from Elasticsearch JS client + if (isEsError(e)) { + return response.customError({ statusCode: e.statusCode, body: e }); + } + + // Case: default + return response.internalError({ body: e }); + } + }) ); } diff --git a/x-pack/plugins/watcher/server/routes/api/license/register_refresh_route.ts b/x-pack/plugins/watcher/server/routes/api/license/register_refresh_route.ts index a61fd16e8be4ad..bd537cd6d21ab5 100644 --- a/x-pack/plugins/watcher/server/routes/api/license/register_refresh_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/license/register_refresh_route.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { RequestHandler } from 'kibana/server'; import { RouteDependencies } from '../../../types'; import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; /* @@ -13,16 +12,15 @@ it needs to make a round-trip to the kibana server. This refresh endpoint is pro for when the client needs to check the license, but doesn't need to pull data from the server for any reason, i.e., when adding a new watch. */ -export function registerRefreshRoute(deps: RouteDependencies) { - const handler: RequestHandler = (ctx, request, response) => { - return response.ok({ body: { success: true } }); - }; +export function registerRefreshRoute(deps: RouteDependencies) { deps.router.get( { path: '/api/watcher/license/refresh', validate: false, }, - licensePreRoutingFactory(deps, handler) + licensePreRoutingFactory(deps, (ctx, request, response) => { + return response.ok({ body: { success: true } }); + }) ); } diff --git a/x-pack/plugins/watcher/server/routes/api/register_list_fields_route.ts b/x-pack/plugins/watcher/server/routes/api/register_list_fields_route.ts index 7c47379b875892..d72e5ad2f817de 100644 --- a/x-pack/plugins/watcher/server/routes/api/register_list_fields_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/register_list_fields_route.ts @@ -5,15 +5,18 @@ */ import { schema } from '@kbn/config-schema'; -import { RequestHandler } from 'kibana/server'; -import { callWithRequestFactory } from '../../lib/call_with_request_factory'; +import { IScopedClusterClient } from 'kibana/server'; import { isEsError } from '../../lib/is_es_error'; // @ts-ignore import { Fields } from '../../models/fields/index'; import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory'; import { RouteDependencies } from '../../types'; -function fetchFields(callWithRequest: any, indexes: string[]) { +const bodySchema = schema.object({ + indexes: schema.arrayOf(schema.string()), +}); + +function fetchFields(dataClient: IScopedClusterClient, indexes: string[]) { const params = { index: indexes, fields: ['*'], @@ -22,44 +25,39 @@ function fetchFields(callWithRequest: any, indexes: string[]) { ignore: 404, }; - return callWithRequest('fieldCaps', params); + return dataClient.callAsCurrentUser('fieldCaps', params); } export function registerListFieldsRoute(deps: RouteDependencies) { - const handler: RequestHandler = async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); - const { indexes } = request.body; - - try { - const fieldsResponse = await fetchFields(callWithRequest, indexes); - const json = fieldsResponse.status === 404 ? { fields: [] } : fieldsResponse; - const fields = Fields.fromUpstreamJson(json); - return response.ok({ body: fields.downstreamJson }); - } catch (e) { - // Case: Error from Elasticsearch JS client - if (isEsError(e)) { - return response.customError({ - statusCode: e.statusCode, - body: { - message: e.message, - }, - }); - } - - // Case: default - return response.internalError({ body: e }); - } - }; - deps.router.post( { path: '/api/watcher/fields', validate: { - body: schema.object({ - indexes: schema.arrayOf(schema.string()), - }), + body: bodySchema, }, }, - licensePreRoutingFactory(deps, handler) + licensePreRoutingFactory(deps, async (ctx, request, response) => { + const { indexes } = request.body; + + try { + const fieldsResponse = await fetchFields(ctx.watcher!.client, indexes); + const json = fieldsResponse.status === 404 ? { fields: [] } : fieldsResponse; + const fields = Fields.fromUpstreamJson(json); + return response.ok({ body: fields.downstreamJson }); + } catch (e) { + // Case: Error from Elasticsearch JS client + if (isEsError(e)) { + return response.customError({ + statusCode: e.statusCode, + body: { + message: e.message, + }, + }); + } + + // Case: default + return response.internalError({ body: e }); + } + }) ); } diff --git a/x-pack/plugins/watcher/server/routes/api/register_load_history_route.ts b/x-pack/plugins/watcher/server/routes/api/register_load_history_route.ts index 1be8477df79bc7..8c9068123ce8d7 100644 --- a/x-pack/plugins/watcher/server/routes/api/register_load_history_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/register_load_history_route.ts @@ -6,8 +6,7 @@ import { schema } from '@kbn/config-schema'; import { get } from 'lodash'; -import { RequestHandler } from 'kibana/server'; -import { callWithRequestFactory } from '../../lib/call_with_request_factory'; +import { IScopedClusterClient } from 'kibana/server'; import { isEsError } from '../../lib/is_es_error'; import { INDEX_NAMES } from '../../../common/constants'; import { RouteDependencies } from '../../types'; @@ -15,8 +14,12 @@ import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory' // @ts-ignore import { WatchHistoryItem } from '../../models/watch_history_item/index'; -function fetchHistoryItem(callWithRequest: any, watchHistoryItemId: string) { - return callWithRequest('search', { +const paramsSchema = schema.object({ + id: schema.string(), +}); + +function fetchHistoryItem(dataClient: IScopedClusterClient, watchHistoryItemId: string) { + return dataClient.callAsCurrentUser('search', { index: INDEX_NAMES.WATCHER_HISTORY, body: { query: { @@ -29,49 +32,44 @@ function fetchHistoryItem(callWithRequest: any, watchHistoryItemId: string) { } export function registerLoadHistoryRoute(deps: RouteDependencies) { - const handler: RequestHandler = async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); - const id = request.params.id; - - try { - const responseFromES = await fetchHistoryItem(callWithRequest, id); - const hit = get(responseFromES, 'hits.hits[0]'); - if (!hit) { - return response.notFound({ body: `Watch History Item with id = ${id} not found` }); - } - const watchHistoryItemJson = get(hit, '_source'); - const watchId = get(hit, '_source.watch_id'); - const json = { - id, - watchId, - watchHistoryItemJson, - includeDetails: true, - }; - - const watchHistoryItem = WatchHistoryItem.fromUpstreamJson(json); - return response.ok({ - body: { watchHistoryItem: watchHistoryItem.downstreamJson }, - }); - } catch (e) { - // Case: Error from Elasticsearch JS client - if (isEsError(e)) { - return response.customError({ statusCode: e.statusCode, body: e }); - } - - // Case: default - return response.internalError({ body: e }); - } - }; - deps.router.get( { path: '/api/watcher/history/{id}', validate: { - params: schema.object({ - id: schema.string(), - }), + params: paramsSchema, }, }, - licensePreRoutingFactory(deps, handler) + licensePreRoutingFactory(deps, async (ctx, request, response) => { + const id = request.params.id; + + try { + const responseFromES = await fetchHistoryItem(ctx.watcher!.client, id); + const hit = get(responseFromES, 'hits.hits[0]'); + if (!hit) { + return response.notFound({ body: `Watch History Item with id = ${id} not found` }); + } + const watchHistoryItemJson = get(hit, '_source'); + const watchId = get(hit, '_source.watch_id'); + const json = { + id, + watchId, + watchHistoryItemJson, + includeDetails: true, + }; + + const watchHistoryItem = WatchHistoryItem.fromUpstreamJson(json); + return response.ok({ + body: { watchHistoryItem: watchHistoryItem.downstreamJson }, + }); + } catch (e) { + // Case: Error from Elasticsearch JS client + if (isEsError(e)) { + return response.customError({ statusCode: e.statusCode, body: e }); + } + + // Case: default + return response.internalError({ body: e }); + } + }) ); } diff --git a/x-pack/plugins/watcher/server/routes/api/settings/register_load_route.ts b/x-pack/plugins/watcher/server/routes/api/settings/register_load_route.ts index 6c70c2d0d07b6a..fe9dd32735692d 100644 --- a/x-pack/plugins/watcher/server/routes/api/settings/register_load_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/settings/register_load_route.ts @@ -4,14 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IClusterClient, RequestHandler } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { isEsError } from '../../../lib/is_es_error'; // @ts-ignore import { Settings } from '../../../models/settings/index'; import { RouteDependencies } from '../../../types'; import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; -function fetchClusterSettings(client: IClusterClient) { +function fetchClusterSettings(client: IScopedClusterClient) { return client.callAsInternalUser('cluster.getSettings', { includeDefaults: true, filterPath: '**.xpack.notification', @@ -19,25 +19,24 @@ function fetchClusterSettings(client: IClusterClient) { } export function registerLoadRoute(deps: RouteDependencies) { - const handler: RequestHandler = async (ctx, request, response) => { - try { - const settings = await fetchClusterSettings(deps.elasticsearch); - return response.ok({ body: Settings.fromUpstreamJson(settings).downstreamJson }); - } catch (e) { - // Case: Error from Elasticsearch JS client - if (isEsError(e)) { - return response.customError({ statusCode: e.statusCode, body: e }); - } - - // Case: default - return response.internalError({ body: e }); - } - }; deps.router.get( { path: '/api/watcher/settings', validate: false, }, - licensePreRoutingFactory(deps, handler) + licensePreRoutingFactory(deps, async (ctx, request, response) => { + try { + const settings = await fetchClusterSettings(ctx.watcher!.client); + return response.ok({ body: Settings.fromUpstreamJson(settings).downstreamJson }); + } catch (e) { + // Case: Error from Elasticsearch JS client + if (isEsError(e)) { + return response.customError({ statusCode: e.statusCode, body: e }); + } + + // Case: default + return response.internalError({ body: e }); + } + }) ); } diff --git a/x-pack/plugins/watcher/server/routes/api/watch/action/register_acknowledge_route.ts b/x-pack/plugins/watcher/server/routes/api/watch/action/register_acknowledge_route.ts index 08eec7456e3a57..9e024a63b82c57 100644 --- a/x-pack/plugins/watcher/server/routes/api/watch/action/register_acknowledge_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/watch/action/register_acknowledge_route.ts @@ -6,60 +6,58 @@ import { schema } from '@kbn/config-schema'; import { get } from 'lodash'; -import { RequestHandler } from 'kibana/server'; -import { callWithRequestFactory } from '../../../../lib/call_with_request_factory'; +import { IScopedClusterClient } from 'kibana/server'; import { isEsError } from '../../../../lib/is_es_error'; // @ts-ignore import { WatchStatus } from '../../../../models/watch_status/index'; import { RouteDependencies } from '../../../../types'; import { licensePreRoutingFactory } from '../../../../lib/license_pre_routing_factory'; -function acknowledgeAction(callWithRequest: any, watchId: string, actionId: string) { - return callWithRequest('watcher.ackWatch', { +const paramsSchema = schema.object({ + watchId: schema.string(), + actionId: schema.string(), +}); + +function acknowledgeAction(dataClient: IScopedClusterClient, watchId: string, actionId: string) { + return dataClient.callAsCurrentUser('watcher.ackWatch', { id: watchId, action: actionId, }); } export function registerAcknowledgeRoute(deps: RouteDependencies) { - const handler: RequestHandler = async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); - const { watchId, actionId } = request.params; - - try { - const hit = await acknowledgeAction(callWithRequest, watchId, actionId); - const watchStatusJson = get(hit, 'status'); - const json = { - id: watchId, - watchStatusJson, - }; - - const watchStatus = WatchStatus.fromUpstreamJson(json); - return response.ok({ - body: { watchStatus: watchStatus.downstreamJson }, - }); - } catch (e) { - // Case: Error from Elasticsearch JS client - if (isEsError(e)) { - const body = e.statusCode === 404 ? `Watch with id = ${watchId} not found` : e; - return response.customError({ statusCode: e.statusCode, body }); - } - - // Case: default - return response.internalError({ body: e }); - } - }; - deps.router.put( { path: '/api/watcher/watch/{watchId}/action/{actionId}/acknowledge', validate: { - params: schema.object({ - watchId: schema.string(), - actionId: schema.string(), - }), + params: paramsSchema, }, }, - licensePreRoutingFactory(deps, handler) + licensePreRoutingFactory(deps, async (ctx, request, response) => { + const { watchId, actionId } = request.params; + + try { + const hit = await acknowledgeAction(ctx.watcher!.client, watchId, actionId); + const watchStatusJson = get(hit, 'status'); + const json = { + id: watchId, + watchStatusJson, + }; + + const watchStatus = WatchStatus.fromUpstreamJson(json); + return response.ok({ + body: { watchStatus: watchStatus.downstreamJson }, + }); + } catch (e) { + // Case: Error from Elasticsearch JS client + if (isEsError(e)) { + const body = e.statusCode === 404 ? `Watch with id = ${watchId} not found` : e; + return response.customError({ statusCode: e.statusCode, body }); + } + + // Case: default + return response.internalError({ body: e }); + } + }) ); } diff --git a/x-pack/plugins/watcher/server/routes/api/watch/register_activate_route.ts b/x-pack/plugins/watcher/server/routes/api/watch/register_activate_route.ts index fdc20854ed8c22..1afeeb4e80efbf 100644 --- a/x-pack/plugins/watcher/server/routes/api/watch/register_activate_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/watch/register_activate_route.ts @@ -5,62 +5,59 @@ */ import { schema } from '@kbn/config-schema'; -import { RequestHandler } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { get } from 'lodash'; -import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; import { isEsError } from '../../../lib/is_es_error'; import { RouteDependencies } from '../../../types'; import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; // @ts-ignore import { WatchStatus } from '../../../models/watch_status/index'; -function activateWatch(callWithRequest: any, watchId: string) { - return callWithRequest('watcher.activateWatch', { +function activateWatch(dataClient: IScopedClusterClient, watchId: string) { + return dataClient.callAsCurrentUser('watcher.activateWatch', { id: watchId, }); } -export function registerActivateRoute(deps: RouteDependencies) { - const handler: RequestHandler = async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); - - const { watchId } = request.params; - - try { - const hit = await activateWatch(callWithRequest, watchId); - const watchStatusJson = get(hit, 'status'); - const json = { - id: watchId, - watchStatusJson, - }; - - const watchStatus = WatchStatus.fromUpstreamJson(json); - return response.ok({ - body: { - watchStatus: watchStatus.downstreamJson, - }, - }); - } catch (e) { - // Case: Error from Elasticsearch JS client - if (isEsError(e)) { - const body = e.statusCode === 404 ? `Watch with id = ${watchId} not found` : e; - return response.customError({ statusCode: e.statusCode, body }); - } - - // Case: default - return response.internalError({ body: e }); - } - }; +const paramsSchema = schema.object({ + watchId: schema.string(), +}); +export function registerActivateRoute(deps: RouteDependencies) { deps.router.put( { path: '/api/watcher/watch/{watchId}/activate', validate: { - params: schema.object({ - watchId: schema.string(), - }), + params: paramsSchema, }, }, - licensePreRoutingFactory(deps, handler) + licensePreRoutingFactory(deps, async (ctx, request, response) => { + const { watchId } = request.params; + + try { + const hit = await activateWatch(ctx.watcher!.client, watchId); + const watchStatusJson = get(hit, 'status'); + const json = { + id: watchId, + watchStatusJson, + }; + + const watchStatus = WatchStatus.fromUpstreamJson(json); + return response.ok({ + body: { + watchStatus: watchStatus.downstreamJson, + }, + }); + } catch (e) { + // Case: Error from Elasticsearch JS client + if (isEsError(e)) { + const body = e.statusCode === 404 ? `Watch with id = ${watchId} not found` : e; + return response.customError({ statusCode: e.statusCode, body }); + } + + // Case: default + return response.internalError({ body: e }); + } + }) ); } diff --git a/x-pack/plugins/watcher/server/routes/api/watch/register_deactivate_route.ts b/x-pack/plugins/watcher/server/routes/api/watch/register_deactivate_route.ts index 08d99f42df0540..3171d8ee2e1e55 100644 --- a/x-pack/plugins/watcher/server/routes/api/watch/register_deactivate_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/watch/register_deactivate_route.ts @@ -3,63 +3,61 @@ * 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'; -import { RequestHandler } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { get } from 'lodash'; -import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; import { isEsError } from '../../../lib/is_es_error'; import { RouteDependencies } from '../../../types'; import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; // @ts-ignore import { WatchStatus } from '../../../models/watch_status/index'; -function deactivateWatch(callWithRequest: any, watchId: string) { - return callWithRequest('watcher.deactivateWatch', { +const paramsSchema = schema.object({ + watchId: schema.string(), +}); + +function deactivateWatch(dataClient: IScopedClusterClient, watchId: string) { + return dataClient.callAsCurrentUser('watcher.deactivateWatch', { id: watchId, }); } export function registerDeactivateRoute(deps: RouteDependencies) { - const handler: RequestHandler = async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); - - const { watchId } = request.params; - - try { - const hit = await deactivateWatch(callWithRequest, watchId); - const watchStatusJson = get(hit, 'status'); - const json = { - id: watchId, - watchStatusJson, - }; - - const watchStatus = WatchStatus.fromUpstreamJson(json); - return response.ok({ - body: { - watchStatus: watchStatus.downstreamJson, - }, - }); - } catch (e) { - // Case: Error from Elasticsearch JS client - if (isEsError(e)) { - const body = e.statusCode === 404 ? `Watch with id = ${watchId} not found` : e; - return response.customError({ statusCode: e.statusCode, body }); - } - - // Case: default - return response.internalError({ body: e }); - } - }; - deps.router.put( { path: '/api/watcher/watch/{watchId}/deactivate', validate: { - params: schema.object({ - watchId: schema.string(), - }), + params: paramsSchema, }, }, - licensePreRoutingFactory(deps, handler) + licensePreRoutingFactory(deps, async (ctx, request, response) => { + const { watchId } = request.params; + + try { + const hit = await deactivateWatch(ctx.watcher!.client, watchId); + const watchStatusJson = get(hit, 'status'); + const json = { + id: watchId, + watchStatusJson, + }; + + const watchStatus = WatchStatus.fromUpstreamJson(json); + return response.ok({ + body: { + watchStatus: watchStatus.downstreamJson, + }, + }); + } catch (e) { + // Case: Error from Elasticsearch JS client + if (isEsError(e)) { + const body = e.statusCode === 404 ? `Watch with id = ${watchId} not found` : e; + return response.customError({ statusCode: e.statusCode, body }); + } + + // Case: default + return response.internalError({ body: e }); + } + }) ); } diff --git a/x-pack/plugins/watcher/server/routes/api/watch/register_delete_route.ts b/x-pack/plugins/watcher/server/routes/api/watch/register_delete_route.ts index 6e95cf959bc9ca..bfdf328550bbea 100644 --- a/x-pack/plugins/watcher/server/routes/api/watch/register_delete_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/watch/register_delete_route.ts @@ -5,49 +5,46 @@ */ import { schema } from '@kbn/config-schema'; -import { RequestHandler } from 'kibana/server'; -import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; +import { IScopedClusterClient } from 'kibana/server'; import { isEsError } from '../../../lib/is_es_error'; import { RouteDependencies } from '../../../types'; import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; -function deleteWatch(callWithRequest: any, watchId: string) { - return callWithRequest('watcher.deleteWatch', { +const paramsSchema = schema.object({ + watchId: schema.string(), +}); + +function deleteWatch(dataClient: IScopedClusterClient, watchId: string) { + return dataClient.callAsCurrentUser('watcher.deleteWatch', { id: watchId, }); } export function registerDeleteRoute(deps: RouteDependencies) { - const handler: RequestHandler = async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); - - const { watchId } = request.params; - - try { - return response.ok({ - body: await deleteWatch(callWithRequest, watchId), - }); - } catch (e) { - // Case: Error from Elasticsearch JS client - if (isEsError(e)) { - const body = e.statusCode === 404 ? `Watch with id = ${watchId} not found` : e; - return response.customError({ statusCode: e.statusCode, body }); - } - - // Case: default - return response.internalError({ body: e }); - } - }; - deps.router.delete( { path: '/api/watcher/watch/{watchId}', validate: { - params: schema.object({ - watchId: schema.string(), - }), + params: paramsSchema, }, }, - licensePreRoutingFactory(deps, handler) + licensePreRoutingFactory(deps, async (ctx, request, response) => { + const { watchId } = request.params; + + try { + return response.ok({ + body: await deleteWatch(ctx.watcher!.client, watchId), + }); + } catch (e) { + // Case: Error from Elasticsearch JS client + if (isEsError(e)) { + const body = e.statusCode === 404 ? `Watch with id = ${watchId} not found` : e; + return response.customError({ statusCode: e.statusCode, body }); + } + + // Case: default + return response.internalError({ body: e }); + } + }) ); } diff --git a/x-pack/plugins/watcher/server/routes/api/watch/register_execute_route.ts b/x-pack/plugins/watcher/server/routes/api/watch/register_execute_route.ts index fef6d07317da5c..7aaa77c05a5f06 100644 --- a/x-pack/plugins/watcher/server/routes/api/watch/register_execute_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/watch/register_execute_route.ts @@ -5,9 +5,8 @@ */ import { schema } from '@kbn/config-schema'; -import { RequestHandler } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { get } from 'lodash'; -import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; import { isEsError } from '../../../lib/is_es_error'; import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; @@ -19,60 +18,63 @@ import { Watch } from '../../../models/watch/index'; // @ts-ignore import { WatchHistoryItem } from '../../../models/watch_history_item/index'; -function executeWatch(callWithRequest: any, executeDetails: any, watchJson: any) { +const bodySchema = schema.object({ + executeDetails: schema.object({}, { allowUnknowns: true }), + watch: schema.object({}, { allowUnknowns: true }), +}); + +function executeWatch(dataClient: IScopedClusterClient, executeDetails: any, watchJson: any) { const body = executeDetails; body.watch = watchJson; - return callWithRequest('watcher.executeWatch', { + return dataClient.callAsCurrentUser('watcher.executeWatch', { body, }); } export function registerExecuteRoute(deps: RouteDependencies) { - const handler: RequestHandler = async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); - const executeDetails = ExecuteDetails.fromDownstreamJson(request.body.executeDetails); - const watch = Watch.fromDownstreamJson(request.body.watch); - - try { - const hit = await executeWatch(callWithRequest, executeDetails.upstreamJson, watch.watchJson); - const id = get(hit, '_id'); - const watchHistoryItemJson = get(hit, 'watch_record'); - const watchId = get(hit, 'watch_record.watch_id'); - const json = { - id, - watchId, - watchHistoryItemJson, - includeDetails: true, - }; - - const watchHistoryItem = WatchHistoryItem.fromUpstreamJson(json); - return response.ok({ - body: { - watchHistoryItem: watchHistoryItem.downstreamJson, - }, - }); - } catch (e) { - // Case: Error from Elasticsearch JS client - if (isEsError(e)) { - return response.customError({ statusCode: e.statusCode, body: e }); - } - - // Case: default - return response.internalError({ body: e }); - } - }; - deps.router.put( { path: '/api/watcher/watch/execute', validate: { - body: schema.object({ - executeDetails: schema.object({}, { allowUnknowns: true }), - watch: schema.object({}, { allowUnknowns: true }), - }), + body: bodySchema, }, }, - licensePreRoutingFactory(deps, handler) + licensePreRoutingFactory(deps, async (ctx, request, response) => { + const executeDetails = ExecuteDetails.fromDownstreamJson(request.body.executeDetails); + const watch = Watch.fromDownstreamJson(request.body.watch); + + try { + const hit = await executeWatch( + ctx.watcher!.client, + executeDetails.upstreamJson, + watch.watchJson + ); + const id = get(hit, '_id'); + const watchHistoryItemJson = get(hit, 'watch_record'); + const watchId = get(hit, 'watch_record.watch_id'); + const json = { + id, + watchId, + watchHistoryItemJson, + includeDetails: true, + }; + + const watchHistoryItem = WatchHistoryItem.fromUpstreamJson(json); + return response.ok({ + body: { + watchHistoryItem: watchHistoryItem.downstreamJson, + }, + }); + } catch (e) { + // Case: Error from Elasticsearch JS client + if (isEsError(e)) { + return response.customError({ statusCode: e.statusCode, body: e }); + } + + // Case: default + return response.internalError({ body: e }); + } + }) ); } diff --git a/x-pack/plugins/watcher/server/routes/api/watch/register_history_route.ts b/x-pack/plugins/watcher/server/routes/api/watch/register_history_route.ts index 7f0f1ac8d66a31..b64c28e114b72c 100644 --- a/x-pack/plugins/watcher/server/routes/api/watch/register_history_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/watch/register_history_route.ts @@ -5,9 +5,8 @@ */ import { schema } from '@kbn/config-schema'; -import { RequestHandler } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { get } from 'lodash'; -import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; import { fetchAllFromScroll } from '../../../lib/fetch_all_from_scroll'; import { INDEX_NAMES, ES_SCROLL_SETTINGS } from '../../../../common/constants'; import { isEsError } from '../../../lib/is_es_error'; @@ -16,7 +15,15 @@ import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_facto // @ts-ignore import { WatchHistoryItem } from '../../../models/watch_history_item/index'; -function fetchHistoryItems(callWithRequest: any, watchId: any, startTime: any) { +const paramsSchema = schema.object({ + watchId: schema.string(), +}); + +const querySchema = schema.object({ + startTime: schema.string(), +}); + +function fetchHistoryItems(dataClient: IScopedClusterClient, watchId: any, startTime: any) { const params: any = { index: INDEX_NAMES.WATCHER_HISTORY, scroll: ES_SCROLL_SETTINGS.KEEPALIVE, @@ -37,61 +44,57 @@ function fetchHistoryItems(callWithRequest: any, watchId: any, startTime: any) { params.body.query.bool.must.push(timeRangeQuery); } - return callWithRequest('search', params).then((response: any) => - fetchAllFromScroll(response, callWithRequest) - ); + return dataClient + .callAsCurrentUser('search', params) + .then((response: any) => fetchAllFromScroll(response, dataClient)); } export function registerHistoryRoute(deps: RouteDependencies) { - const handler: RequestHandler = async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); - const { watchId } = request.params; - const { startTime } = request.query; - - try { - const hits = await fetchHistoryItems(callWithRequest, watchId, startTime); - const watchHistoryItems = hits.map((hit: any) => { - const id = get(hit, '_id'); - const watchHistoryItemJson = get(hit, '_source'); - - const opts = { includeDetails: false }; - return WatchHistoryItem.fromUpstreamJson( - { - id, - watchId, - watchHistoryItemJson, - }, - opts - ); - }); - - return response.ok({ - body: { - watchHistoryItems: watchHistoryItems.map( - (watchHistoryItem: any) => watchHistoryItem.downstreamJson - ), - }, - }); - } catch (e) { - // Case: Error from Elasticsearch JS client - if (isEsError(e)) { - return response.customError({ statusCode: e.statusCode, body: e }); - } - - // Case: default - return response.internalError({ body: e }); - } - }; - deps.router.get( { path: '/api/watcher/watch/{watchId}/history', validate: { - params: schema.object({ - watchId: schema.string(), - }), + params: paramsSchema, + query: querySchema, }, }, - licensePreRoutingFactory(deps, handler) + licensePreRoutingFactory(deps, async (ctx, request, response) => { + const { watchId } = request.params; + const { startTime } = request.query; + + try { + const hits = await fetchHistoryItems(ctx.watcher!.client, watchId, startTime); + const watchHistoryItems = hits.map((hit: any) => { + const id = get(hit, '_id'); + const watchHistoryItemJson = get(hit, '_source'); + + const opts = { includeDetails: false }; + return WatchHistoryItem.fromUpstreamJson( + { + id, + watchId, + watchHistoryItemJson, + }, + opts + ); + }); + + return response.ok({ + body: { + watchHistoryItems: watchHistoryItems.map( + (watchHistoryItem: any) => watchHistoryItem.downstreamJson + ), + }, + }); + } catch (e) { + // Case: Error from Elasticsearch JS client + if (isEsError(e)) { + return response.customError({ statusCode: e.statusCode, body: e }); + } + + // Case: default + return response.internalError({ body: e }); + } + }) ); } diff --git a/x-pack/plugins/watcher/server/routes/api/watch/register_load_route.ts b/x-pack/plugins/watcher/server/routes/api/watch/register_load_route.ts index 91d71cd737121f..6363054921333f 100644 --- a/x-pack/plugins/watcher/server/routes/api/watch/register_load_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/watch/register_load_route.ts @@ -5,65 +5,63 @@ */ import { schema } from '@kbn/config-schema'; -import { RequestHandler } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { get } from 'lodash'; -import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; import { isEsError } from '../../../lib/is_es_error'; import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; // @ts-ignore import { Watch } from '../../../models/watch/index'; import { RouteDependencies } from '../../../types'; -function fetchWatch(callWithRequest: any, watchId: string) { - return callWithRequest('watcher.getWatch', { +const paramsSchema = schema.object({ + id: schema.string(), +}); + +function fetchWatch(dataClient: IScopedClusterClient, watchId: string) { + return dataClient.callAsCurrentUser('watcher.getWatch', { id: watchId, }); } export function registerLoadRoute(deps: RouteDependencies) { - const handler: RequestHandler = async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); - - const id = request.params.id; - - try { - const hit = await fetchWatch(callWithRequest, id); - const watchJson = get(hit, 'watch'); - const watchStatusJson = get(hit, 'status'); - const json = { - id, - watchJson, - watchStatusJson, - }; - - const watch = Watch.fromUpstreamJson(json, { - throwExceptions: { - Action: false, - }, - }); - return response.ok({ - body: { watch: watch.downstreamJson }, - }); - } catch (e) { - // Case: Error from Elasticsearch JS client - if (isEsError(e)) { - const body = e.statusCode === 404 ? `Watch with id = ${id} not found` : e; - return response.customError({ statusCode: e.statusCode, body }); - } - - // Case: default - return response.internalError({ body: e }); - } - }; deps.router.get( { path: '/api/watcher/watch/{id}', validate: { - params: schema.object({ - id: schema.string(), - }), + params: paramsSchema, }, }, - licensePreRoutingFactory(deps, handler) + licensePreRoutingFactory(deps, async (ctx, request, response) => { + const id = request.params.id; + + try { + const hit = await fetchWatch(ctx.watcher!.client, id); + const watchJson = get(hit, 'watch'); + const watchStatusJson = get(hit, 'status'); + const json = { + id, + watchJson, + watchStatusJson, + }; + + const watch = Watch.fromUpstreamJson(json, { + throwExceptions: { + Action: false, + }, + }); + return response.ok({ + body: { watch: watch.downstreamJson }, + }); + } catch (e) { + // Case: Error from Elasticsearch JS client + if (isEsError(e)) { + const body = e.statusCode === 404 ? `Watch with id = ${id} not found` : e; + return response.customError({ statusCode: e.statusCode, body }); + } + + // Case: default + return response.internalError({ body: e }); + } + }) ); } diff --git a/x-pack/plugins/watcher/server/routes/api/watch/register_save_route.ts b/x-pack/plugins/watcher/server/routes/api/watch/register_save_route.ts index 7986424e6229a7..572790f12a5f8a 100644 --- a/x-pack/plugins/watcher/server/routes/api/watch/register_save_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/watch/register_save_route.ts @@ -5,98 +5,104 @@ */ import { schema } from '@kbn/config-schema'; -import { RequestHandler } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { i18n } from '@kbn/i18n'; import { WATCH_TYPES } from '../../../../common/constants'; import { serializeJsonWatch, serializeThresholdWatch } from '../../../../common/lib/serialization'; -import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; import { isEsError } from '../../../lib/is_es_error'; import { RouteDependencies } from '../../../types'; import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; -function fetchWatch(callWithRequest: any, watchId: string) { - return callWithRequest('watcher.getWatch', { +const paramsSchema = schema.object({ + id: schema.string(), +}); + +const bodySchema = schema.object( + { + type: schema.string(), + isNew: schema.boolean(), + }, + { allowUnknowns: true } +); + +function fetchWatch(dataClient: IScopedClusterClient, watchId: string) { + return dataClient.callAsCurrentUser('watcher.getWatch', { id: watchId, }); } -function saveWatch(callWithRequest: any, id: string, body: any) { - return callWithRequest('watcher.putWatch', { +function saveWatch(dataClient: IScopedClusterClient, id: string, body: any) { + return dataClient.callAsCurrentUser('watcher.putWatch', { id, body, }); } export function registerSaveRoute(deps: RouteDependencies) { - const handler: RequestHandler = async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); - const { id } = request.params; - const { type, isNew, ...watchConfig } = request.body; + deps.router.put( + { + path: '/api/watcher/watch/{id}', + validate: { + params: paramsSchema, + body: bodySchema, + }, + }, + licensePreRoutingFactory(deps, async (ctx, request, response) => { + const { id } = request.params; + const { type, isNew, ...watchConfig } = request.body; - // For new watches, verify watch with the same ID doesn't already exist - if (isNew) { - try { - const existingWatch = await fetchWatch(callWithRequest, id); - if (existingWatch.found) { - return response.conflict({ - body: { - message: i18n.translate('xpack.watcher.saveRoute.duplicateWatchIdErrorMessage', { - defaultMessage: "There is already a watch with ID '{watchId}'.", - values: { - watchId: id, - }, - }), - }, - }); - } - } catch (e) { - const es404 = isEsError(e) && e.statusCode === 404; - if (!es404) { - return response.internalError({ body: e }); + // For new watches, verify watch with the same ID doesn't already exist + if (isNew) { + try { + const existingWatch = await fetchWatch(ctx.watcher!.client, id); + if (existingWatch.found) { + return response.conflict({ + body: { + message: i18n.translate('xpack.watcher.saveRoute.duplicateWatchIdErrorMessage', { + defaultMessage: "There is already a watch with ID '{watchId}'.", + values: { + watchId: id, + }, + }), + }, + }); + } + } catch (e) { + const es404 = isEsError(e) && e.statusCode === 404; + if (!es404) { + return response.internalError({ body: e }); + } + // Else continue... } - // Else continue... } - } - let serializedWatch; + let serializedWatch; - switch (type) { - case WATCH_TYPES.JSON: - const { name, watch } = watchConfig; - serializedWatch = serializeJsonWatch(name, watch); - break; + switch (type) { + case WATCH_TYPES.JSON: + const { name, watch } = watchConfig as any; + serializedWatch = serializeJsonWatch(name, watch); + break; - case WATCH_TYPES.THRESHOLD: - serializedWatch = serializeThresholdWatch(watchConfig); - break; - } - - try { - // Create new watch - return response.ok({ - body: await saveWatch(callWithRequest, id, serializedWatch), - }); - } catch (e) { - // Case: Error from Elasticsearch JS client - if (isEsError(e)) { - return response.customError({ statusCode: e.statusCode, body: e }); + case WATCH_TYPES.THRESHOLD: + serializedWatch = serializeThresholdWatch(watchConfig); + break; } - // Case: default - return response.internalError({ body: e }); - } - }; + try { + // Create new watch + return response.ok({ + body: await saveWatch(ctx.watcher!.client, id, serializedWatch), + }); + } catch (e) { + // Case: Error from Elasticsearch JS client + if (isEsError(e)) { + return response.customError({ statusCode: e.statusCode, body: e }); + } - deps.router.put( - { - path: '/api/watcher/watch/{id}', - validate: { - params: schema.object({ - id: schema.string(), - }), - body: schema.object({}, { allowUnknowns: true }), - }, - }, - licensePreRoutingFactory(deps, handler) + // Case: default + return response.internalError({ body: e }); + } + }) ); } diff --git a/x-pack/plugins/watcher/server/routes/api/watch/register_visualize_route.ts b/x-pack/plugins/watcher/server/routes/api/watch/register_visualize_route.ts index f2110bcc0ebdbc..200b35953b6f23 100644 --- a/x-pack/plugins/watcher/server/routes/api/watch/register_visualize_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/watch/register_visualize_route.ts @@ -5,8 +5,7 @@ */ import { schema } from '@kbn/config-schema'; -import { RequestHandler } from 'kibana/server'; -import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; +import { IScopedClusterClient } from 'kibana/server'; import { isEsError } from '../../../lib/is_es_error'; import { RouteDependencies } from '../../../types'; import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; @@ -16,7 +15,12 @@ import { Watch } from '../../../models/watch/index'; // @ts-ignore import { VisualizeOptions } from '../../../models/visualize_options/index'; -function fetchVisualizeData(callWithRequest: any, index: any, body: any) { +const bodySchema = schema.object({ + watch: schema.object({}, { allowUnknowns: true }), + options: schema.object({}, { allowUnknowns: true }), +}); + +function fetchVisualizeData(dataClient: IScopedClusterClient, index: any, body: any) { const params = { index, body, @@ -25,46 +29,40 @@ function fetchVisualizeData(callWithRequest: any, index: any, body: any) { ignore: [404], }; - return callWithRequest('search', params); + return dataClient.callAsCurrentUser('search', params); } export function registerVisualizeRoute(deps: RouteDependencies) { - const handler: RequestHandler = async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); - const watch = Watch.fromDownstreamJson(request.body.watch); - const options = VisualizeOptions.fromDownstreamJson(request.body.options); - const body = watch.getVisualizeQuery(options); - - try { - const hits = await fetchVisualizeData(callWithRequest, watch.index, body); - const visualizeData = watch.formatVisualizeData(hits); - - return response.ok({ - body: { - visualizeData, - }, - }); - } catch (e) { - // Case: Error from Elasticsearch JS client - if (isEsError(e)) { - return response.customError({ statusCode: e.statusCode, body: e }); - } - - // Case: default - return response.internalError({ body: e }); - } - }; - deps.router.post( { path: '/api/watcher/watch/visualize', validate: { - body: schema.object({ - watch: schema.object({}, { allowUnknowns: true }), - options: schema.object({}, { allowUnknowns: true }), - }), + body: bodySchema, }, }, - licensePreRoutingFactory(deps, handler) + licensePreRoutingFactory(deps, async (ctx, request, response) => { + const watch = Watch.fromDownstreamJson(request.body.watch); + const options = VisualizeOptions.fromDownstreamJson(request.body.options); + const body = watch.getVisualizeQuery(options); + + try { + const hits = await fetchVisualizeData(ctx.watcher!.client, watch.index, body); + const visualizeData = watch.formatVisualizeData(hits); + + return response.ok({ + body: { + visualizeData, + }, + }); + } catch (e) { + // Case: Error from Elasticsearch JS client + if (isEsError(e)) { + return response.customError({ statusCode: e.statusCode, body: e }); + } + + // Case: default + return response.internalError({ body: e }); + } + }) ); } diff --git a/x-pack/plugins/watcher/server/routes/api/watches/register_delete_route.ts b/x-pack/plugins/watcher/server/routes/api/watches/register_delete_route.ts index 2ac824529f9a6d..71e0a77bff9729 100644 --- a/x-pack/plugins/watcher/server/routes/api/watches/register_delete_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/watches/register_delete_route.ts @@ -5,16 +5,20 @@ */ import { schema } from '@kbn/config-schema'; -import { RequestHandler } from 'kibana/server'; -import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; +import { IScopedClusterClient } from 'kibana/server'; import { RouteDependencies } from '../../../types'; import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; -function deleteWatches(callWithRequest: any, watchIds: string[]) { +const bodySchema = schema.object({ + watchIds: schema.arrayOf(schema.string()), +}); + +function deleteWatches(dataClient: IScopedClusterClient, watchIds: string[]) { const deletePromises = watchIds.map(watchId => { - return callWithRequest('watcher.deleteWatch', { - id: watchId, - }) + return dataClient + .callAsCurrentUser('watcher.deleteWatch', { + id: watchId, + }) .then((success: Array<{ _id: string }>) => ({ success })) .catch((error: Array<{ _id: string }>) => ({ error })); }); @@ -22,7 +26,7 @@ function deleteWatches(callWithRequest: any, watchIds: string[]) { return Promise.all(deletePromises).then(results => { const errors: Error[] = []; const successes: boolean[] = []; - results.forEach(({ success, error }) => { + results.forEach(({ success, error }: { success?: any; error?: any }) => { if (success) { successes.push(success._id); } else if (error) { @@ -38,26 +42,20 @@ function deleteWatches(callWithRequest: any, watchIds: string[]) { } export function registerDeleteRoute(deps: RouteDependencies) { - const handler: RequestHandler = async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); - - try { - const results = await deleteWatches(callWithRequest, request.body.watchIds); - return response.ok({ body: { results } }); - } catch (e) { - return response.internalError({ body: e }); - } - }; - deps.router.post( { path: '/api/watcher/watches/delete', validate: { - body: schema.object({ - watchIds: schema.arrayOf(schema.string()), - }), + body: bodySchema, }, }, - licensePreRoutingFactory(deps, handler) + licensePreRoutingFactory(deps, async (ctx, request, response) => { + try { + const results = await deleteWatches(ctx.watcher!.client, request.body.watchIds); + return response.ok({ body: { results } }); + } catch (e) { + return response.internalError({ body: e }); + } + }) ); } diff --git a/x-pack/plugins/watcher/server/routes/api/watches/register_list_route.ts b/x-pack/plugins/watcher/server/routes/api/watches/register_list_route.ts index fcbdf688a2ab40..5e823a0a8d2de6 100644 --- a/x-pack/plugins/watcher/server/routes/api/watches/register_list_route.ts +++ b/x-pack/plugins/watcher/server/routes/api/watches/register_list_route.ts @@ -4,9 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { RequestHandler } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { get } from 'lodash'; -import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; import { fetchAllFromScroll } from '../../../lib/fetch_all_from_scroll'; import { INDEX_NAMES, ES_SCROLL_SETTINGS } from '../../../../common/constants'; import { isEsError } from '../../../lib/is_es_error'; @@ -15,7 +14,7 @@ import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_facto // @ts-ignore import { Watch } from '../../../models/watch/index'; -function fetchWatches(callWithRequest: any) { +function fetchWatches(dataClient: IScopedClusterClient) { const params = { index: INDEX_NAMES.WATCHES, scroll: ES_SCROLL_SETTINGS.KEEPALIVE, @@ -25,62 +24,58 @@ function fetchWatches(callWithRequest: any) { ignore: [404], }; - return callWithRequest('search', params).then((response: any) => - fetchAllFromScroll(response, callWithRequest) - ); + return dataClient + .callAsCurrentUser('search', params) + .then((response: any) => fetchAllFromScroll(response, dataClient)); } export function registerListRoute(deps: RouteDependencies) { - const handler: RequestHandler = async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); - - try { - const hits = await fetchWatches(callWithRequest); - const watches = hits.map((hit: any) => { - const id = get(hit, '_id'); - const watchJson = get(hit, '_source'); - const watchStatusJson = get(hit, '_source.status'); + deps.router.get( + { + path: '/api/watcher/watches', + validate: false, + }, + licensePreRoutingFactory(deps, async (ctx, request, response) => { + try { + const hits = await fetchWatches(ctx.watcher!.client); + const watches = hits.map((hit: any) => { + const id = get(hit, '_id'); + const watchJson = get(hit, '_source'); + const watchStatusJson = get(hit, '_source.status'); - return Watch.fromUpstreamJson( - { - id, - watchJson, - watchStatusJson, - }, - { - throwExceptions: { - Action: false, + return Watch.fromUpstreamJson( + { + id, + watchJson, + watchStatusJson, }, - } - ); - }); + { + throwExceptions: { + Action: false, + }, + } + ); + }); - return response.ok({ - body: { - watches: watches.map((watch: any) => watch.downstreamJson), - }, - }); - } catch (e) { - // Case: Error from Elasticsearch JS client - if (isEsError(e)) { - return response.customError({ - statusCode: e.statusCode, + return response.ok({ body: { - message: e.message, + watches: watches.map((watch: any) => watch.downstreamJson), }, }); - } - - // Case: default - return response.internalError({ body: e }); - } - }; + } catch (e) { + // Case: Error from Elasticsearch JS client + if (isEsError(e)) { + return response.customError({ + statusCode: e.statusCode, + body: { + message: e.message, + }, + }); + } - deps.router.get( - { - path: '/api/watcher/watches', - validate: false, - }, - licensePreRoutingFactory(deps, handler) + // Case: default + return response.internalError({ body: e }); + } + }) ); } diff --git a/x-pack/plugins/watcher/server/types.ts b/x-pack/plugins/watcher/server/types.ts index d9f2d3c3b1e7a6..dd941054114a84 100644 --- a/x-pack/plugins/watcher/server/types.ts +++ b/x-pack/plugins/watcher/server/types.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IRouter, ElasticsearchServiceSetup, IClusterClient } from 'kibana/server'; +import { IRouter } from 'kibana/server'; import { LicensingPluginSetup } from '../../licensing/server'; import { XPackMainPlugin } from '../../../legacy/plugins/xpack_main/server/xpack_main'; @@ -24,8 +24,6 @@ export interface ServerShim { export interface RouteDependencies { router: IRouter; getLicenseStatus: () => LicenseStatus; - elasticsearchService: ElasticsearchServiceSetup; - elasticsearch: IClusterClient; } export interface LicenseStatus { From 16eb81628f7a56af2e826cf97d8a7e2db77910fb Mon Sep 17 00:00:00 2001 From: Robert Oskamp Date: Thu, 20 Feb 2020 13:31:04 +0100 Subject: [PATCH 5/9] [ML] Transform functional tests - bootstrap transform job for clone test (#57992) This PR adds transform bootstrapping to the functional test transform.api service and demonstrates the usage in a new cloning test file, which will be completed as part of the transform cloning PR. --- .../test/functional/apps/transform/cloning.ts | 65 +++++++++++++ .../test/functional/apps/transform/index.ts | 1 + .../functional/services/transform_ui/api.ts | 91 +++++++++++++++++++ 3 files changed, 157 insertions(+) create mode 100644 x-pack/test/functional/apps/transform/cloning.ts diff --git a/x-pack/test/functional/apps/transform/cloning.ts b/x-pack/test/functional/apps/transform/cloning.ts new file mode 100644 index 00000000000000..f06dc0a14a383d --- /dev/null +++ b/x-pack/test/functional/apps/transform/cloning.ts @@ -0,0 +1,65 @@ +/* + * 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 { TransformPivotConfig } from '../../../../legacy/plugins/transform/public/app/common'; + +function getTransformConfig(): TransformPivotConfig { + const date = Date.now(); + return { + id: `ec_2_${date}`, + source: { index: ['ecommerce'] }, + pivot: { + group_by: { category: { terms: { field: 'category.keyword' } } }, + aggregations: { 'products.base_price.avg': { avg: { field: 'products.base_price' } } }, + }, + description: + 'ecommerce batch transform with avg(products.base_price) grouped by terms(category.keyword)', + dest: { index: `user-ec_2_${date}` }, + }; +} + +export default function({ getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const transform = getService('transform'); + + describe('cloning', function() { + this.tags(['smoke']); + const transformConfig = getTransformConfig(); + + before(async () => { + await esArchiver.load('ml/ecommerce'); + await transform.api.createAndRunTransform(transformConfig); + await transform.securityUI.loginAsTransformPowerUser(); + }); + + after(async () => { + await esArchiver.unload('ml/ecommerce'); + await transform.api.deleteIndices(transformConfig.dest.index); + await transform.api.cleanTransformIndices(); + }); + + const testDataList = [ + { + suiteTitle: 'batch transform with terms group and avg agg', + expected: {}, + }, + ]; + + for (const testData of testDataList) { + describe(`${testData.suiteTitle}`, function() { + after(async () => { + // await transform.api.deleteIndices(); + }); + + it('loads the home page', async () => { + await transform.navigation.navigateTo(); + await transform.management.assertTransformListPageExists(); + }); + }); + } + }); +} diff --git a/x-pack/test/functional/apps/transform/index.ts b/x-pack/test/functional/apps/transform/index.ts index 66a55105b3ca8e..60b72f122f1131 100644 --- a/x-pack/test/functional/apps/transform/index.ts +++ b/x-pack/test/functional/apps/transform/index.ts @@ -23,5 +23,6 @@ export default function({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./creation_index_pattern')); loadTestFile(require.resolve('./creation_saved_search')); + loadTestFile(require.resolve('./cloning')); }); } diff --git a/x-pack/test/functional/services/transform_ui/api.ts b/x-pack/test/functional/services/transform_ui/api.ts index a6756e5940d72b..6a4a1dfff6ea16 100644 --- a/x-pack/test/functional/services/transform_ui/api.ts +++ b/x-pack/test/functional/services/transform_ui/api.ts @@ -7,10 +7,17 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; +import { + TRANSFORM_STATE, + TransformPivotConfig, + TransformStats, +} from '../../../../legacy/plugins/transform/public/app/common'; + export function TransformAPIProvider({ getService }: FtrProviderContext) { const es = getService('legacyEs'); const log = getService('log'); const retry = getService('retry'); + const esSupertest = getService('esSupertest'); return { async deleteIndices(indices: string) { @@ -39,5 +46,89 @@ export function TransformAPIProvider({ getService }: FtrProviderContext) { async cleanTransformIndices() { await this.deleteIndices('.transform-*'); }, + + async getTransformStats(transformId: string): Promise { + log.debug(`Fetching transform stats for transform ${transformId}`); + const statsResponse = await esSupertest + .get(`/_transform/${transformId}/_stats`) + .expect(200) + .then((res: any) => res.body); + + expect(statsResponse.transforms).to.have.length(1); + return statsResponse.transforms[0]; + }, + + async getTransformState(transformId: string): Promise { + const stats = await this.getTransformStats(transformId); + const state: TRANSFORM_STATE = stats.state; + + return state; + }, + + async waitForTransformState(transformId: string, expectedState: TRANSFORM_STATE) { + await retry.waitForWithTimeout( + `transform state to be ${expectedState}`, + 2 * 60 * 1000, + async () => { + const state = await this.getTransformState(transformId); + if (state === expectedState) { + return true; + } else { + throw new Error(`expected transform state to be ${expectedState} but got ${state}`); + } + } + ); + }, + + async waitForBatchTransformToComplete(transformId: string) { + await retry.waitForWithTimeout(`batch transform to complete`, 2 * 60 * 1000, async () => { + const stats = await this.getTransformStats(transformId); + if (stats.state === TRANSFORM_STATE.STOPPED && stats.checkpointing.last.checkpoint === 1) { + return true; + } else { + throw new Error( + `expected batch transform to be stopped with last checkpoint = 1 (got status: '${stats.state}', checkpoint: '${stats.checkpointing.last.checkpoint}')` + ); + } + }); + }, + + async getTransform(transformId: string) { + return await esSupertest.get(`/_transform/${transformId}`).expect(200); + }, + + async createTransform(transformConfig: TransformPivotConfig) { + const transformId = transformConfig.id; + log.debug(`Creating transform with id '${transformId}'...`); + await esSupertest + .put(`/_transform/${transformId}`) + .send(transformConfig) + .expect(200); + + await retry.waitForWithTimeout(`'${transformId}' to be created`, 5 * 1000, async () => { + if (await this.getTransform(transformId)) { + return true; + } else { + throw new Error(`expected transform '${transformId}' to be created`); + } + }); + }, + + async startTransform(transformId: string) { + log.debug(`Starting transform '${transformId}' ...`); + await esSupertest.post(`/_transform/${transformId}/_start`).expect(200); + }, + + async createAndRunTransform(transformConfig: TransformPivotConfig) { + await this.createTransform(transformConfig); + await this.startTransform(transformConfig.id); + if (transformConfig.sync === undefined) { + // batch mode + await this.waitForBatchTransformToComplete(transformConfig.id); + } else { + // continuous mode + await this.waitForTransformState(transformConfig.id, TRANSFORM_STATE.STARTED); + } + }, }; } From ed2ca68d796fbb99d219336cd3dddc6ac8d71ba7 Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Thu, 20 Feb 2020 15:40:00 +0300 Subject: [PATCH 6/9] [ui/agg_response/tabify] -> TypeScript & Jest & Shim (#57660) * [ui/agg_response/tabify] -> TypeScript & Jest & Shim [ui/agg_response/tabify] -> TypeScript & Jest & Shim Part of #57394 * fix CI * move tabify into new folder * TypeScript _bucket.js * rename _buckets -> bucket * fix CI * tabify.test.js -> tabify.test.ts * tabify.js -> tabify.ts * fix JEST * Update src/legacy/core_plugins/data/public/search/tabify/types.ts Co-Authored-By: Luke Elmers * fake_hierarchical_data.js -> fake_hierarchical_data.ts * TimeRange -> TabbedRangeFilterParams Co-authored-by: Elastic Machine Co-authored-by: Luke Elmers --- ...ical_data.js => fake_hierarchical_data.ts} | 16 +- src/legacy/core_plugins/data/public/index.ts | 2 + .../data/public/search/aggs/agg_configs.ts | 6 +- .../data/public/search/aggs/agg_params.ts | 4 +- .../data/public/search/aggs/index.ts | 3 + .../data/public/search/expressions/esaggs.ts | 3 +- .../core_plugins/data/public/search/index.ts | 1 + .../public/search/tabify/buckets.test.ts} | 112 +++++----- .../data/public/search/tabify/buckets.ts | 135 ++++++++++++ .../public/search/tabify/get_columns.test.ts | 191 +++++++++++++++++ .../data/public/search/tabify/get_columns.ts} | 4 +- .../data/public/search/tabify/index.ts} | 1 + .../search/tabify/response_writer.test.ts | 170 +++++++++++++++ .../public/search/tabify/response_writer.ts | 88 ++++++++ .../data/public/search/tabify/tabify.test.ts | 172 +++++++++++++++ .../data/public/search/tabify/tabify.ts | 173 +++++++++++++++ .../data/public/search/tabify/types.ts} | 18 +- .../data/public/search/utils/types.ts | 6 + .../kibana/public/discover/kibana_services.ts | 3 +- .../public/agg_table/__tests__/agg_table.js | 12 +- .../agg_table/__tests__/agg_table_group.js | 6 +- .../vis_type_table/public/legacy_imports.ts | 4 +- .../vis_type_vislib/public/legacy_imports.ts | 4 +- .../__tests__/visualizations/pie_chart.js | 6 +- src/legacy/ui/public/agg_response/index.js | 2 +- .../tabify/__tests__/_get_columns.js | 199 ------------------ .../tabify/__tests__/_integration.js | 175 --------------- .../tabify/__tests__/_response_writer.js | 186 ---------------- .../ui/public/agg_response/tabify/_buckets.js | 123 ----------- .../agg_response/tabify/_response_writer.js | 97 --------- .../ui/public/agg_response/tabify/tabify.js | 134 ------------ .../es_geo_grid_source/es_geo_grid_source.js | 2 +- 32 files changed, 1053 insertions(+), 1005 deletions(-) rename src/fixtures/{fake_hierarchical_data.js => fake_hierarchical_data.ts} (98%) rename src/legacy/{ui/public/agg_response/tabify/__tests__/_buckets.js => core_plugins/data/public/search/tabify/buckets.test.ts} (66%) create mode 100644 src/legacy/core_plugins/data/public/search/tabify/buckets.ts create mode 100644 src/legacy/core_plugins/data/public/search/tabify/get_columns.test.ts rename src/legacy/{ui/public/agg_response/tabify/_get_columns.ts => core_plugins/data/public/search/tabify/get_columns.ts} (96%) rename src/legacy/{ui/public/agg_response/tabify/index.js => core_plugins/data/public/search/tabify/index.ts} (94%) create mode 100644 src/legacy/core_plugins/data/public/search/tabify/response_writer.test.ts create mode 100644 src/legacy/core_plugins/data/public/search/tabify/response_writer.ts create mode 100644 src/legacy/core_plugins/data/public/search/tabify/tabify.test.ts create mode 100644 src/legacy/core_plugins/data/public/search/tabify/tabify.ts rename src/legacy/{ui/public/agg_response/tabify/__tests__/tabify.js => core_plugins/data/public/search/tabify/types.ts} (69%) delete mode 100644 src/legacy/ui/public/agg_response/tabify/__tests__/_get_columns.js delete mode 100644 src/legacy/ui/public/agg_response/tabify/__tests__/_integration.js delete mode 100644 src/legacy/ui/public/agg_response/tabify/__tests__/_response_writer.js delete mode 100644 src/legacy/ui/public/agg_response/tabify/_buckets.js delete mode 100644 src/legacy/ui/public/agg_response/tabify/_response_writer.js delete mode 100644 src/legacy/ui/public/agg_response/tabify/tabify.js diff --git a/src/fixtures/fake_hierarchical_data.js b/src/fixtures/fake_hierarchical_data.ts similarity index 98% rename from src/fixtures/fake_hierarchical_data.js rename to src/fixtures/fake_hierarchical_data.ts index b4ae02a487049f..4480caae39664a 100644 --- a/src/fixtures/fake_hierarchical_data.js +++ b/src/fixtures/fake_hierarchical_data.ts @@ -17,16 +17,14 @@ * under the License. */ -const data = {}; - -data.metricOnly = { +export const metricOnly = { hits: { total: 1000, hits: [], max_score: 0 }, aggregations: { agg_1: { value: 412032 }, }, }; -data.threeTermBuckets = { +export const threeTermBuckets = { hits: { total: 1000, hits: [], max_score: 0 }, aggregations: { agg_2: { @@ -129,7 +127,7 @@ data.threeTermBuckets = { }, }; -data.oneTermOneHistogramBucketWithTwoMetricsOneTopHitOneDerivative = { +export const oneTermOneHistogramBucketWithTwoMetricsOneTopHitOneDerivative = { hits: { total: 1000, hits: [], max_score: 0 }, aggregations: { agg_3: { @@ -520,7 +518,7 @@ data.oneTermOneHistogramBucketWithTwoMetricsOneTopHitOneDerivative = { }, }; -data.oneRangeBucket = { +export const oneRangeBucket = { took: 35, timed_out: false, _shards: { @@ -555,7 +553,7 @@ data.oneRangeBucket = { }, }; -data.oneFilterBucket = { +export const oneFilterBucket = { took: 11, timed_out: false, _shards: { @@ -582,7 +580,7 @@ data.oneFilterBucket = { }, }; -data.oneHistogramBucket = { +export const oneHistogramBucket = { took: 37, timed_out: false, _shards: { @@ -632,5 +630,3 @@ data.oneHistogramBucket = { }, }, }; - -export default data; diff --git a/src/legacy/core_plugins/data/public/index.ts b/src/legacy/core_plugins/data/public/index.ts index 50120292a627a6..ce46f534141f4f 100644 --- a/src/legacy/core_plugins/data/public/index.ts +++ b/src/legacy/core_plugins/data/public/index.ts @@ -81,4 +81,6 @@ export { // search_source getRequestInspectorStats, getResponseInspectorStats, + tabifyAggResponse, + tabifyGetColumns, } from './search'; diff --git a/src/legacy/core_plugins/data/public/search/aggs/agg_configs.ts b/src/legacy/core_plugins/data/public/search/aggs/agg_configs.ts index 7e7e4944b00da9..8e091ed5f21ae0 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/agg_configs.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/agg_configs.ts @@ -27,7 +27,7 @@ */ import _ from 'lodash'; -import { AggConfig, AggConfigOptions } from './agg_config'; +import { AggConfig, AggConfigOptions, IAggConfig } from './agg_config'; import { Schema } from './schemas'; import { AggGroupNames } from './agg_groups'; import { @@ -63,7 +63,7 @@ export class AggConfigs { public schemas: any; public timeRange?: TimeRange; - aggs: AggConfig[]; + aggs: IAggConfig[]; constructor(indexPattern: IndexPattern, configStates = [] as any, schemas?: any) { configStates = AggConfig.ensureIds(configStates); @@ -74,7 +74,7 @@ export class AggConfigs { configStates.forEach((params: any) => this.createAggConfig(params)); - if (this.schemas) { + if (schemas) { this.initializeDefaultsFromSchemas(schemas); } } diff --git a/src/legacy/core_plugins/data/public/search/aggs/agg_params.ts b/src/legacy/core_plugins/data/public/search/aggs/agg_params.ts index 34727ff4614b95..551cb81529a0a3 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/agg_params.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/agg_params.ts @@ -76,7 +76,9 @@ export const writeParams = < aggs?: IAggConfigs, locals?: Record ) => { - const output = { params: {} as Record }; + const output: Record = { + params: {} as Record, + }; locals = locals || {}; params.forEach(param => { diff --git a/src/legacy/core_plugins/data/public/search/aggs/index.ts b/src/legacy/core_plugins/data/public/search/aggs/index.ts index 0fef7f38aae742..0bdb92b8de65e8 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/index.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/index.ts @@ -50,3 +50,6 @@ export { isValidJson, isValidInterval } from './utils'; export { BUCKET_TYPES } from './buckets/bucket_agg_types'; export { METRIC_TYPES } from './metrics/metric_agg_types'; export { ISchemas, Schema, Schemas } from './schemas'; + +// types +export { IAggConfig, IAggConfigs } from './types'; 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 9aee7124c95211..302527e4ed549f 100644 --- a/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts +++ b/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts @@ -39,8 +39,7 @@ import { import { buildTabularInspectorData } from './build_tabular_inspector_data'; import { calculateObjectHash } from '../../../../visualizations/public'; -// @ts-ignore -import { tabifyAggResponse } from '../../../../../ui/public/agg_response/tabify/tabify'; +import { tabifyAggResponse } from '../../../../../core_plugins/data/public'; import { PersistedState } from '../../../../../ui/public/persisted_state'; import { Adapters } from '../../../../../../plugins/inspector/public'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths diff --git a/src/legacy/core_plugins/data/public/search/index.ts b/src/legacy/core_plugins/data/public/search/index.ts index 90e191b769a8df..96d2825559da26 100644 --- a/src/legacy/core_plugins/data/public/search/index.ts +++ b/src/legacy/core_plugins/data/public/search/index.ts @@ -20,3 +20,4 @@ export * from './aggs'; export { getRequestInspectorStats, getResponseInspectorStats } from './utils'; export { serializeAggConfig } from './expressions/utils'; +export { tabifyAggResponse, tabifyGetColumns } from './tabify'; diff --git a/src/legacy/ui/public/agg_response/tabify/__tests__/_buckets.js b/src/legacy/core_plugins/data/public/search/tabify/buckets.test.ts similarity index 66% rename from src/legacy/ui/public/agg_response/tabify/__tests__/_buckets.js rename to src/legacy/core_plugins/data/public/search/tabify/buckets.test.ts index b85b45d3c5820a..ef2748102623ab 100644 --- a/src/legacy/ui/public/agg_response/tabify/__tests__/_buckets.js +++ b/src/legacy/core_plugins/data/public/search/tabify/buckets.test.ts @@ -17,31 +17,36 @@ * under the License. */ -import expect from '@kbn/expect'; -import { TabifyBuckets } from '../_buckets'; +import { TabifyBuckets } from './buckets'; +import { AggGroupNames } from '../aggs'; -describe('Buckets wrapper', function() { - function test(aggResp, count, keys) { - it('reads the length', function() { +jest.mock('ui/new_platform'); + +describe('Buckets wrapper', () => { + const check = (aggResp: any, count: number, keys: string[]) => { + test('reads the length', () => { const buckets = new TabifyBuckets(aggResp); - expect(buckets).to.have.length(count); + expect(buckets).toHaveLength(count); }); - it('iterates properly, passing in the key', function() { + test('iterates properly, passing in the key', () => { const buckets = new TabifyBuckets(aggResp); - const keysSent = []; - buckets.forEach(function(bucket, key) { - keysSent.push(key); + const keysSent: any[] = []; + + buckets.forEach((bucket, key) => { + if (key) { + keysSent.push(key); + } }); - expect(keysSent).to.have.length(count); - expect(keysSent).to.eql(keys); + expect(keysSent).toHaveLength(count); + expect(keysSent).toEqual(keys); }); - } + }; - describe('with object style buckets', function() { - const aggResp = { - buckets: { + describe('with object style buckets', () => { + let aggResp: any = { + [AggGroupNames.Buckets]: { '0-100': {}, '100-200': {}, '200-300': {}, @@ -51,11 +56,11 @@ describe('Buckets wrapper', function() { const count = 3; const keys = ['0-100', '100-200', '200-300']; - test(aggResp, count, keys); + check(aggResp, count, keys); - it('should accept filters agg queries with strings', () => { - const aggResp = { - buckets: { + test('should accept filters agg queries with strings', () => { + aggResp = { + [AggGroupNames.Buckets]: { 'response:200': {}, 'response:404': {}, }, @@ -75,15 +80,17 @@ describe('Buckets wrapper', function() { }; const buckets = new TabifyBuckets(aggResp, aggParams); - expect(buckets).to.have.length(2); + + expect(buckets).toHaveLength(2); + buckets._keys.forEach(key => { - expect(key).to.be.a('string'); + expect(typeof key).toBe('string'); }); }); - it('should accept filters agg queries with query_string queries', () => { - const aggResp = { - buckets: { + test('should accept filters agg queries with query_string queries', () => { + aggResp = { + [AggGroupNames.Buckets]: { 'response:200': {}, 'response:404': {}, }, @@ -103,15 +110,17 @@ describe('Buckets wrapper', function() { }; const buckets = new TabifyBuckets(aggResp, aggParams); - expect(buckets).to.have.length(2); + + expect(buckets).toHaveLength(2); + buckets._keys.forEach(key => { - expect(key).to.be.a('string'); + expect(typeof key).toBe('string'); }); }); - it('should accept filters agg queries with query dsl queries', () => { - const aggResp = { - buckets: { + test('should accept filters agg queries with query dsl queries', () => { + aggResp = { + [AggGroupNames.Buckets]: { '{match_all: {}}': {}, }, }; @@ -126,16 +135,18 @@ describe('Buckets wrapper', function() { }; const buckets = new TabifyBuckets(aggResp, aggParams); - expect(buckets).to.have.length(1); + + expect(buckets).toHaveLength(1); + buckets._keys.forEach(key => { - expect(key).to.be.a('string'); + expect(typeof key).toBe('string'); }); }); }); - describe('with array style buckets', function() { + describe('with array style buckets', () => { const aggResp = { - buckets: [ + [AggGroupNames.Buckets]: [ { key: '0-100', value: {} }, { key: '100-200', value: {} }, { key: '200-300', value: {} }, @@ -145,23 +156,24 @@ describe('Buckets wrapper', function() { const count = 3; const keys = ['0-100', '100-200', '200-300']; - test(aggResp, count, keys); + check(aggResp, count, keys); }); - describe('with single bucket aggregations (filter)', function() { - it('creates single bucket from agg content', function() { + describe('with single bucket aggregations (filter)', () => { + test('creates single bucket from agg content', () => { const aggResp = { single_bucket: {}, doc_count: 5, }; const buckets = new TabifyBuckets(aggResp); - expect(buckets).to.have.length(1); + + expect(buckets).toHaveLength(1); }); }); - describe('drop_partial option', function() { + describe('drop_partial option', () => { const aggResp = { - buckets: [ + [AggGroupNames.Buckets]: [ { key: 0, value: {} }, { key: 100, value: {} }, { key: 200, value: {} }, @@ -169,7 +181,7 @@ describe('Buckets wrapper', function() { ], }; - it('drops partial buckets when enabled', function() { + test('drops partial buckets when enabled', () => { const aggParams = { drop_partials: true, field: { @@ -182,10 +194,11 @@ describe('Buckets wrapper', function() { name: 'date', }; const buckets = new TabifyBuckets(aggResp, aggParams, timeRange); - expect(buckets).to.have.length(1); + + expect(buckets).toHaveLength(1); }); - it('keeps partial buckets when disabled', function() { + test('keeps partial buckets when disabled', () => { const aggParams = { drop_partials: false, field: { @@ -198,10 +211,11 @@ describe('Buckets wrapper', function() { name: 'date', }; const buckets = new TabifyBuckets(aggResp, aggParams, timeRange); - expect(buckets).to.have.length(4); + + expect(buckets).toHaveLength(4); }); - it('keeps aligned buckets when enabled', function() { + test('keeps aligned buckets when enabled', () => { const aggParams = { drop_partials: true, field: { @@ -214,10 +228,11 @@ describe('Buckets wrapper', function() { name: 'date', }; const buckets = new TabifyBuckets(aggResp, aggParams, timeRange); - expect(buckets).to.have.length(3); + + expect(buckets).toHaveLength(3); }); - it('does not drop buckets for non-timerange fields', function() { + test('does not drop buckets for non-timerange fields', () => { const aggParams = { drop_partials: true, field: { @@ -230,7 +245,8 @@ describe('Buckets wrapper', function() { name: 'date', }; const buckets = new TabifyBuckets(aggResp, aggParams, timeRange); - expect(buckets).to.have.length(4); + + expect(buckets).toHaveLength(4); }); }); }); diff --git a/src/legacy/core_plugins/data/public/search/tabify/buckets.ts b/src/legacy/core_plugins/data/public/search/tabify/buckets.ts new file mode 100644 index 00000000000000..8078136299f8c6 --- /dev/null +++ b/src/legacy/core_plugins/data/public/search/tabify/buckets.ts @@ -0,0 +1,135 @@ +/* + * 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 { get, isPlainObject, keys, findKey } from 'lodash'; +import moment from 'moment'; +import { IAggConfig } from '../aggs'; +import { TabbedRangeFilterParams } from './types'; +import { AggResponseBucket } from '../types'; + +type AggParams = IAggConfig['params'] & { + drop_partials: boolean; + ranges: TabbedRangeFilterParams[]; +}; + +const isRangeEqual = (range1: TabbedRangeFilterParams, range2: TabbedRangeFilterParams) => + range1?.from === range2?.from && range1?.to === range2?.to; + +export class TabifyBuckets { + length: number; + objectMode: boolean; + buckets: any; + _keys: any[] = []; + + constructor(aggResp: any, aggParams?: AggParams, timeRange?: TabbedRangeFilterParams) { + if (aggResp && aggResp.buckets) { + this.buckets = aggResp.buckets; + } else if (aggResp) { + // Some Bucket Aggs only return a single bucket (like filter). + // In those instances, the aggResp is the content of the single bucket. + this.buckets = [aggResp]; + } else { + this.buckets = []; + } + + this.objectMode = isPlainObject(this.buckets); + + if (this.objectMode) { + this._keys = keys(this.buckets); + this.length = this._keys.length; + } else { + this.length = this.buckets.length; + } + + if (this.length && aggParams) { + this.orderBucketsAccordingToParams(aggParams); + if (aggParams.drop_partials) { + this.dropPartials(aggParams, timeRange); + } + } + } + + forEach(fn: (bucket: any, key: any) => void) { + const buckets = this.buckets; + + if (this.objectMode) { + this._keys.forEach(key => { + fn(buckets[key], key); + }); + } else { + buckets.forEach((bucket: AggResponseBucket) => { + fn(bucket, bucket.key); + }); + } + } + + private orderBucketsAccordingToParams(params: AggParams) { + if (params.filters && this.objectMode) { + this._keys = params.filters.map((filter: any) => { + const query = get(filter, 'input.query.query_string.query', filter.input.query); + const queryString = typeof query === 'string' ? query : JSON.stringify(query); + + return filter.label || queryString || '*'; + }); + } else if (params.ranges && this.objectMode) { + this._keys = params.ranges.map((range: TabbedRangeFilterParams) => + findKey(this.buckets, (el: TabbedRangeFilterParams) => isRangeEqual(el, range)) + ); + } else if (params.ranges && params.field.type !== 'date') { + let ranges = params.ranges; + if (params.ipRangeType) { + ranges = params.ipRangeType === 'mask' ? ranges.mask : ranges.fromTo; + } + this.buckets = ranges.map((range: any) => { + if (range.mask) { + return this.buckets.find((el: AggResponseBucket) => el.key === range.mask); + } + + return this.buckets.find((el: TabbedRangeFilterParams) => isRangeEqual(el, range)); + }); + } + } + + // dropPartials should only be called if the aggParam setting is enabled, + // and the agg field is the same as the Time Range. + private dropPartials(params: AggParams, timeRange?: TabbedRangeFilterParams) { + if ( + !timeRange || + this.buckets.length <= 1 || + this.objectMode || + params.field.name !== timeRange.name + ) { + return; + } + + const interval = this.buckets[1].key - this.buckets[0].key; + + this.buckets = this.buckets.filter((bucket: AggResponseBucket) => { + if (moment(bucket.key).isBefore(timeRange.gte)) { + return false; + } + if (moment(bucket.key + interval).isAfter(timeRange.lte)) { + return false; + } + return true; + }); + + this.length = this.buckets.length; + } +} diff --git a/src/legacy/core_plugins/data/public/search/tabify/get_columns.test.ts b/src/legacy/core_plugins/data/public/search/tabify/get_columns.test.ts new file mode 100644 index 00000000000000..0328e87d8b8329 --- /dev/null +++ b/src/legacy/core_plugins/data/public/search/tabify/get_columns.test.ts @@ -0,0 +1,191 @@ +/* + * 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 { tabifyGetColumns, AggColumn } from './get_columns'; +import { AggConfigs, AggGroupNames, Schemas } from '../aggs'; + +jest.mock('ui/new_platform'); + +describe('get columns', () => { + const createAggConfigs = (aggs: any[] = []) => { + const field = { + name: '@timestamp', + }; + + const indexPattern = { + id: '1234', + title: 'logstash-*', + fields: { + getByName: () => field, + filter: () => [field], + }, + } as any; + + return new AggConfigs( + indexPattern, + aggs, + new Schemas([ + { + group: AggGroupNames.Metrics, + name: 'metric', + min: 1, + defaults: [{ schema: 'metric', type: 'count' }], + }, + ]).all + ); + }; + + test('should inject a count metric if no aggs exist', () => { + const columns = tabifyGetColumns(createAggConfigs().aggs, true); + + expect(columns).toHaveLength(1); + expect(columns[0]).toHaveProperty('aggConfig'); + expect(columns[0].aggConfig.type).toHaveProperty('name', 'count'); + }); + + test('should inject a count metric if only buckets exist', () => { + const columns = tabifyGetColumns( + createAggConfigs([ + { + type: 'date_histogram', + schema: 'segment', + params: { field: '@timestamp', interval: '10s' }, + }, + ]).aggs, + true + ); + + expect(columns).toHaveLength(2); + expect(columns[1]).toHaveProperty('aggConfig'); + expect(columns[1].aggConfig.type).toHaveProperty('name', 'count'); + }); + + test('should inject the metric after each bucket if the vis is hierarchical', () => { + const columns = tabifyGetColumns( + createAggConfigs([ + { + type: 'date_histogram', + schema: 'segment', + params: { field: '@timestamp', interval: '10s' }, + }, + { + type: 'date_histogram', + schema: 'segment', + params: { field: '@timestamp', interval: '10s' }, + }, + { + type: 'date_histogram', + schema: 'segment', + params: { field: '@timestamp', interval: '10s' }, + }, + { + type: 'date_histogram', + schema: 'segment', + params: { field: '@timestamp', interval: '10s' }, + }, + ]).aggs, + false + ); + + expect(columns).toHaveLength(8); + + columns.forEach((column, i) => { + expect(column).toHaveProperty('aggConfig'); + expect(column.aggConfig.type).toHaveProperty('name', i % 2 ? 'count' : 'date_histogram'); + }); + }); + + test('should inject the multiple metrics after each bucket if the vis is hierarchical', () => { + const columns = tabifyGetColumns( + createAggConfigs([ + { + type: 'date_histogram', + schema: 'segment', + params: { field: '@timestamp', interval: '10s' }, + }, + { type: 'avg', schema: 'metric', params: { field: 'bytes' } }, + { + type: 'date_histogram', + schema: 'segment', + params: { field: '@timestamp', interval: '10s' }, + }, + { + type: 'date_histogram', + schema: 'segment', + params: { field: '@timestamp', interval: '10s' }, + }, + { type: 'sum', schema: 'metric', params: { field: 'bytes' } }, + { + type: 'date_histogram', + schema: 'segment', + params: { field: '@timestamp', interval: '10s' }, + }, + ]).aggs, + false + ); + + function checkColumns(column: AggColumn, i: number) { + expect(column).toHaveProperty('aggConfig'); + + switch (i) { + case 0: + expect(column.aggConfig.type).toHaveProperty('name', 'date_histogram'); + break; + case 1: + expect(column.aggConfig.type).toHaveProperty('name', 'avg'); + break; + case 2: + expect(column.aggConfig.type).toHaveProperty('name', 'sum'); + break; + } + } + + expect(columns).toHaveLength(12); + + for (let i = 0; i < columns.length; i += 3) { + columns.slice(i, i + 3).forEach(checkColumns); + } + }); + + test('should put all metrics at the end of the columns if the vis is not hierarchical', () => { + const columns = tabifyGetColumns( + createAggConfigs([ + { + type: 'date_histogram', + schema: 'segment', + params: { field: '@timestamp', interval: '20s' }, + }, + { type: 'sum', schema: 'metric', params: { field: '@timestamp' } }, + { + type: 'date_histogram', + schema: 'segment', + params: { field: '@timestamp', interval: '10s' }, + }, + ]).aggs, + false + ); + + expect(columns.map(c => c.name)).toEqual([ + '@timestamp per 20 seconds', + 'Sum of @timestamp', + '@timestamp per 10 seconds', + 'Sum of @timestamp', + ]); + }); +}); diff --git a/src/legacy/ui/public/agg_response/tabify/_get_columns.ts b/src/legacy/core_plugins/data/public/search/tabify/get_columns.ts similarity index 96% rename from src/legacy/ui/public/agg_response/tabify/_get_columns.ts rename to src/legacy/core_plugins/data/public/search/tabify/get_columns.ts index 4144d5be16012a..54f09f6c6364fa 100644 --- a/src/legacy/ui/public/agg_response/tabify/_get_columns.ts +++ b/src/legacy/core_plugins/data/public/search/tabify/get_columns.ts @@ -18,7 +18,7 @@ */ import { groupBy } from 'lodash'; -import { IAggConfig } from '../../agg_types'; +import { IAggConfig } from '../aggs'; export interface AggColumn { aggConfig: IAggConfig; @@ -40,7 +40,7 @@ const getColumn = (agg: IAggConfig, i: number): AggColumn => { * @param {AggConfigs} aggs - the agg configs object to which the aggregation response correlates * @param {boolean} minimalColumns - setting to true will only return a column for the last bucket/metric instead of one for each level */ -export function tabifyGetColumns(aggs: IAggConfig[], minimalColumns: boolean) { +export function tabifyGetColumns(aggs: IAggConfig[], minimalColumns: boolean): AggColumn[] { // pick the columns if (minimalColumns) { return aggs.map((agg, i) => getColumn(agg, i)); diff --git a/src/legacy/ui/public/agg_response/tabify/index.js b/src/legacy/core_plugins/data/public/search/tabify/index.ts similarity index 94% rename from src/legacy/ui/public/agg_response/tabify/index.js rename to src/legacy/core_plugins/data/public/search/tabify/index.ts index f14ca647e4b32a..be8d64510033c4 100644 --- a/src/legacy/ui/public/agg_response/tabify/index.js +++ b/src/legacy/core_plugins/data/public/search/tabify/index.ts @@ -18,3 +18,4 @@ */ export { tabifyAggResponse } from './tabify'; +export { tabifyGetColumns } from './get_columns'; diff --git a/src/legacy/core_plugins/data/public/search/tabify/response_writer.test.ts b/src/legacy/core_plugins/data/public/search/tabify/response_writer.test.ts new file mode 100644 index 00000000000000..f5df0a683ca00c --- /dev/null +++ b/src/legacy/core_plugins/data/public/search/tabify/response_writer.test.ts @@ -0,0 +1,170 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { TabbedAggResponseWriter } from './response_writer'; +import { AggConfigs, AggGroupNames, Schemas, BUCKET_TYPES } from '../aggs'; + +import { TabbedResponseWriterOptions } from './types'; + +jest.mock('ui/new_platform'); + +describe('TabbedAggResponseWriter class', () => { + let responseWriter: TabbedAggResponseWriter; + + const splitAggConfig = [ + { + type: BUCKET_TYPES.TERMS, + params: { + field: 'geo.src', + }, + }, + ]; + + const twoSplitsAggConfig = [ + { + type: BUCKET_TYPES.TERMS, + params: { + field: 'geo.src', + }, + }, + { + type: BUCKET_TYPES.TERMS, + params: { + field: 'machine.os.raw', + }, + }, + ]; + + const createResponseWritter = (aggs: any[] = [], opts?: Partial) => { + const field = { + name: 'geo.src', + }; + + const indexPattern = { + id: '1234', + title: 'logstash-*', + fields: { + getByName: () => field, + filter: () => [field], + }, + } as any; + + return new TabbedAggResponseWriter( + new AggConfigs( + indexPattern, + aggs, + new Schemas([ + { + group: AggGroupNames.Metrics, + name: 'metric', + min: 1, + defaults: [{ schema: 'metric', type: 'count' }], + }, + ]).all + ), + { + metricsAtAllLevels: false, + partialRows: false, + ...opts, + } + ); + }; + + describe('Constructor', () => { + beforeEach(() => { + responseWriter = createResponseWritter(twoSplitsAggConfig); + }); + + test('generates columns', () => { + expect(responseWriter.columns.length).toEqual(3); + }); + + test('correctly generates columns with metricsAtAllLevels set to true', () => { + const minimalColumnsResponseWriter = createResponseWritter(twoSplitsAggConfig, { + metricsAtAllLevels: true, + }); + + expect(minimalColumnsResponseWriter.columns.length).toEqual(4); + }); + + describe('row()', () => { + beforeEach(() => { + responseWriter = createResponseWritter(splitAggConfig); + }); + + test('adds the row to the array', () => { + responseWriter.bucketBuffer = [{ id: 'col-0', value: 'US' }]; + responseWriter.metricBuffer = [{ id: 'col-1', value: 5 }]; + + responseWriter.row(); + + expect(responseWriter.rows.length).toEqual(1); + expect(responseWriter.rows[0]).toEqual({ 'col-0': 'US', 'col-1': 5 }); + }); + + test("doesn't add an empty row", () => { + responseWriter.row(); + + expect(responseWriter.rows.length).toEqual(0); + }); + }); + + describe('response()', () => { + beforeEach(() => { + responseWriter = createResponseWritter(splitAggConfig); + }); + + test('produces correct response', () => { + responseWriter.bucketBuffer = [ + { id: 'col-0-1', value: 'US' }, + { id: 'col-1-2', value: 5 }, + ]; + responseWriter.row(); + + const response = responseWriter.response(); + + expect(response).toHaveProperty('rows'); + expect(response.rows).toEqual([{ 'col-0-1': 'US', 'col-1-2': 5 }]); + expect(response).toHaveProperty('columns'); + expect(response.columns.length).toEqual(2); + expect(response.columns[0]).toHaveProperty('id', 'col-0-1'); + expect(response.columns[0]).toHaveProperty('name', 'geo.src: Descending'); + expect(response.columns[0]).toHaveProperty('aggConfig'); + expect(response.columns[1]).toHaveProperty('id', 'col-1-2'); + expect(response.columns[1]).toHaveProperty('name', 'Count'); + expect(response.columns[1]).toHaveProperty('aggConfig'); + }); + + test('produces correct response for no data', () => { + const response = responseWriter.response(); + + expect(response).toHaveProperty('rows'); + expect(response.rows.length).toBe(0); + expect(response).toHaveProperty('columns'); + expect(response.columns.length).toEqual(2); + expect(response.columns[0]).toHaveProperty('id', 'col-0-1'); + expect(response.columns[0]).toHaveProperty('name', 'geo.src: Descending'); + expect(response.columns[0]).toHaveProperty('aggConfig'); + expect(response.columns[1]).toHaveProperty('id', 'col-1-2'); + expect(response.columns[1]).toHaveProperty('name', 'Count'); + expect(response.columns[1]).toHaveProperty('aggConfig'); + }); + }); + }); +}); diff --git a/src/legacy/core_plugins/data/public/search/tabify/response_writer.ts b/src/legacy/core_plugins/data/public/search/tabify/response_writer.ts new file mode 100644 index 00000000000000..4c4578e505b71a --- /dev/null +++ b/src/legacy/core_plugins/data/public/search/tabify/response_writer.ts @@ -0,0 +1,88 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { isEmpty } from 'lodash'; +import { IAggConfigs } from '../aggs/agg_configs'; +import { AggColumn, tabifyGetColumns } from './get_columns'; + +import { TabbedResponseWriterOptions } from './types'; + +interface TabbedAggColumn { + id: string; + value: string | number; +} + +type TabbedAggRow = Record; + +/** + * Writer class that collects information about an aggregation response and + * produces a table, or a series of tables. + */ +export class TabbedAggResponseWriter { + columns: AggColumn[]; + rows: TabbedAggRow[] = []; + bucketBuffer: TabbedAggColumn[] = []; + metricBuffer: TabbedAggColumn[] = []; + + private readonly partialRows: boolean; + + /** + * @param {AggConfigs} aggs - the agg configs object to which the aggregation response correlates + * @param {boolean} metricsAtAllLevels - setting to true will produce metrics for every bucket + * @param {boolean} partialRows - setting to true will not remove rows with missing values + */ + constructor( + aggs: IAggConfigs, + { metricsAtAllLevels = false, partialRows = false }: Partial + ) { + this.partialRows = partialRows; + + this.columns = tabifyGetColumns(aggs.getResponseAggs(), !metricsAtAllLevels); + this.rows = []; + } + + /** + * Create a new row by reading the row buffer and bucketBuffer + */ + row() { + const rowBuffer: TabbedAggRow = {}; + + this.bucketBuffer.forEach(bucket => { + rowBuffer[bucket.id] = bucket.value; + }); + + this.metricBuffer.forEach(metric => { + rowBuffer[metric.id] = metric.value; + }); + + const isPartialRow = + this.partialRows && !this.columns.every(column => rowBuffer.hasOwnProperty(column.id)); + + if (!isEmpty(rowBuffer) && !isPartialRow) { + this.rows.push(rowBuffer); + } + } + + response() { + return { + columns: this.columns, + rows: this.rows, + }; + } +} diff --git a/src/legacy/core_plugins/data/public/search/tabify/tabify.test.ts b/src/legacy/core_plugins/data/public/search/tabify/tabify.test.ts new file mode 100644 index 00000000000000..13fe7719b0a856 --- /dev/null +++ b/src/legacy/core_plugins/data/public/search/tabify/tabify.test.ts @@ -0,0 +1,172 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { IndexPattern } from '../../../../../../plugins/data/public'; +import { tabifyAggResponse } from './tabify'; +import { IAggConfig, IAggConfigs, AggGroupNames, Schemas, AggConfigs } from '../aggs'; +import { metricOnly, threeTermBuckets } from 'fixtures/fake_hierarchical_data'; + +jest.mock('ui/new_platform'); + +describe('tabifyAggResponse Integration', () => { + const createAggConfigs = (aggs: IAggConfig[] = []) => { + const field = { + name: '@timestamp', + }; + + const indexPattern = ({ + id: '1234', + title: 'logstash-*', + fields: { + getByName: () => field, + filter: () => [field], + }, + } as unknown) as IndexPattern; + + return new AggConfigs( + indexPattern, + aggs, + new Schemas([ + { + group: AggGroupNames.Metrics, + name: 'metric', + min: 1, + defaults: [{ schema: 'metric', type: 'count' }], + }, + ]).all + ); + }; + + const mockAggConfig = (agg: any): IAggConfig => (agg as unknown) as IAggConfig; + + test('transforms a simple response properly', () => { + const aggConfigs = createAggConfigs(); + + const resp = tabifyAggResponse(aggConfigs, metricOnly, { + metricsAtAllLevels: true, + }); + + expect(resp).toHaveProperty('rows'); + expect(resp).toHaveProperty('columns'); + + expect(resp.rows).toHaveLength(1); + expect(resp.columns).toHaveLength(1); + + expect(resp.rows[0]).toEqual({ 'col-0-1': 1000 }); + expect(resp.columns[0]).toHaveProperty('aggConfig', aggConfigs.aggs[0]); + }); + + describe('transforms a complex response', () => { + let esResp: typeof threeTermBuckets; + let aggConfigs: IAggConfigs; + let avg: IAggConfig; + let ext: IAggConfig; + let src: IAggConfig; + let os: IAggConfig; + + beforeEach(() => { + aggConfigs = createAggConfigs([ + mockAggConfig({ type: 'avg', schema: 'metric', params: { field: '@timestamp' } }), + mockAggConfig({ type: 'terms', schema: 'split', params: { field: '@timestamp' } }), + mockAggConfig({ type: 'terms', schema: 'segment', params: { field: '@timestamp' } }), + mockAggConfig({ type: 'terms', schema: 'segment', params: { field: '@timestamp' } }), + ]); + + [avg, ext, src, os] = aggConfigs.aggs; + + esResp = threeTermBuckets; + esResp.aggregations.agg_2.buckets[1].agg_3.buckets[0].agg_4.buckets = []; + }); + + // check that the columns of a table are formed properly + function expectColumns(table: ReturnType, aggs: IAggConfig[]) { + expect(table.columns).toHaveLength(aggs.length); + + aggs.forEach((agg, i) => { + expect(table.columns[i]).toHaveProperty('aggConfig', agg); + }); + } + + // check that a row has expected values + function expectRow( + row: Record, + asserts: Array<(val: string | number) => void> + ) { + expect(typeof row).toBe('object'); + + asserts.forEach((assert, i: number) => { + if (row[`col-${i}`]) { + assert(row[`col-${i}`]); + } + }); + } + + // check for two character country code + function expectCountry(val: string | number) { + expect(typeof val).toBe('string'); + expect(val).toHaveLength(2); + } + + // check for an OS term + function expectExtension(val: string | number) { + expect(val).toMatch(/^(js|png|html|css|jpg)$/); + } + + // check for an OS term + function expectOS(val: string | number) { + expect(val).toMatch(/^(win|mac|linux)$/); + } + + // check for something like an average bytes result + function expectAvgBytes(val: string | number) { + expect(typeof val).toBe('number'); + expect(val === 0 || val > 1000).toBeDefined(); + } + + test('for non-hierarchical vis', () => { + // the default for a non-hierarchical vis is to display + // only complete rows, and only put the metrics at the end. + + const tabbed = tabifyAggResponse(aggConfigs, esResp, { metricsAtAllLevels: false }); + + expectColumns(tabbed, [ext, src, os, avg]); + + tabbed.rows.forEach(row => { + expectRow(row, [expectExtension, expectCountry, expectOS, expectAvgBytes]); + }); + }); + + test('for hierarchical vis', () => { + const tabbed = tabifyAggResponse(aggConfigs, esResp, { metricsAtAllLevels: true }); + + expectColumns(tabbed, [ext, avg, src, avg, os, avg]); + + tabbed.rows.forEach(row => { + expectRow(row, [ + expectExtension, + expectAvgBytes, + expectCountry, + expectAvgBytes, + expectOS, + expectAvgBytes, + ]); + }); + }); + }); +}); diff --git a/src/legacy/core_plugins/data/public/search/tabify/tabify.ts b/src/legacy/core_plugins/data/public/search/tabify/tabify.ts new file mode 100644 index 00000000000000..078d3f7f72759b --- /dev/null +++ b/src/legacy/core_plugins/data/public/search/tabify/tabify.ts @@ -0,0 +1,173 @@ +/* + * 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 { get } from 'lodash'; +import { TabbedAggResponseWriter } from './response_writer'; +import { TabifyBuckets } from './buckets'; +import { TabbedResponseWriterOptions, TabbedRangeFilterParams } from './types'; +import { AggResponseBucket } from '../types'; +import { IAggConfigs, AggGroupNames } from '../aggs'; + +/** + * Sets up the ResponseWriter and kicks off bucket collection. + */ +export function tabifyAggResponse( + aggConfigs: IAggConfigs, + esResponse: Record, + respOpts?: Partial +) { + /** + * read an aggregation from a bucket, which *might* be found at key (if + * the response came in object form), and will recurse down the aggregation + * tree and will pass the read values to the ResponseWriter. + */ + function collectBucket( + aggs: IAggConfigs, + write: TabbedAggResponseWriter, + bucket: AggResponseBucket, + key: string, + aggScale: number + ) { + const column = write.columns.shift(); + + if (column) { + const agg = column.aggConfig; + const aggInfo = agg.write(aggs); + aggScale *= aggInfo.metricScale || 1; + + switch (agg.type.type) { + case AggGroupNames.Buckets: + const aggBucket = get(bucket, agg.id); + const tabifyBuckets = new TabifyBuckets(aggBucket, agg.params, timeRange); + + if (tabifyBuckets.length) { + tabifyBuckets.forEach((subBucket, tabifyBucketKey) => { + // if the bucket doesn't have value don't add it to the row + // we don't want rows like: { column1: undefined, column2: 10 } + const bucketValue = agg.getKey(subBucket, tabifyBucketKey); + const hasBucketValue = typeof bucketValue !== 'undefined'; + + if (hasBucketValue) { + write.bucketBuffer.push({ id: column.id, value: bucketValue }); + } + + collectBucket( + aggs, + write, + subBucket, + agg.getKey(subBucket, tabifyBucketKey), + aggScale + ); + + if (hasBucketValue) { + write.bucketBuffer.pop(); + } + }); + } else if (respOpts?.partialRows) { + // we don't have any buckets, but we do have metrics at this + // level, then pass all the empty buckets and jump back in for + // the metrics. + write.columns.unshift(column); + passEmptyBuckets(aggs, write, bucket, key, aggScale); + write.columns.shift(); + } else { + // we don't have any buckets, and we don't have isHierarchical + // data, so no metrics, just try to write the row + write.row(); + } + break; + case AggGroupNames.Metrics: + let value = agg.getValue(bucket); + // since the aggregation could be a non integer (such as a max date) + // only do the scaling calculation if it is needed. + if (aggScale !== 1) { + value *= aggScale; + } + write.metricBuffer.push({ id: column.id, value }); + + if (!write.columns.length) { + // row complete + write.row(); + } else { + // process the next agg at this same level + collectBucket(aggs, write, bucket, key, aggScale); + } + + write.metricBuffer.pop(); + + break; + } + + write.columns.unshift(column); + } + } + + // write empty values for each bucket agg, then write + // the metrics from the initial bucket using collectBucket() + function passEmptyBuckets( + aggs: IAggConfigs, + write: TabbedAggResponseWriter, + bucket: AggResponseBucket, + key: string, + aggScale: number + ) { + const column = write.columns.shift(); + + if (column) { + const agg = column.aggConfig; + + switch (agg.type.type) { + case AggGroupNames.Metrics: + // pass control back to collectBucket() + write.columns.unshift(column); + collectBucket(aggs, write, bucket, key, aggScale); + return; + + case AggGroupNames.Buckets: + passEmptyBuckets(aggs, write, bucket, key, aggScale); + } + + write.columns.unshift(column); + } + } + + const write = new TabbedAggResponseWriter(aggConfigs, respOpts || {}); + const topLevelBucket: AggResponseBucket = { + ...esResponse.aggregations, + doc_count: esResponse.hits.total, + }; + + let timeRange: TabbedRangeFilterParams | undefined; + + // Extract the time range object if provided + if (respOpts && respOpts.timeRange) { + const [timeRangeKey] = Object.keys(respOpts.timeRange); + + if (timeRangeKey) { + timeRange = { + name: timeRangeKey, + ...respOpts.timeRange[timeRangeKey], + }; + } + } + + collectBucket(aggConfigs, write, topLevelBucket, '', 1); + + return write.response(); +} diff --git a/src/legacy/ui/public/agg_response/tabify/__tests__/tabify.js b/src/legacy/core_plugins/data/public/search/tabify/types.ts similarity index 69% rename from src/legacy/ui/public/agg_response/tabify/__tests__/tabify.js rename to src/legacy/core_plugins/data/public/search/tabify/types.ts index 38ed5408b603e5..3a02a2b64f0c33 100644 --- a/src/legacy/ui/public/agg_response/tabify/__tests__/tabify.js +++ b/src/legacy/core_plugins/data/public/search/tabify/types.ts @@ -17,8 +17,16 @@ * under the License. */ -import './_get_columns'; -import './_buckets'; -import './_response_writer'; -import './_integration'; -describe('Tabify Agg Response', function() {}); +import { RangeFilterParams } from '../../../../../../plugins/data/public'; + +/** @internal **/ +export interface TabbedRangeFilterParams extends RangeFilterParams { + name: string; +} + +/** @internal **/ +export interface TabbedResponseWriterOptions { + metricsAtAllLevels: boolean; + partialRows: boolean; + timeRange?: { [key: string]: RangeFilterParams }; +} diff --git a/src/legacy/core_plugins/data/public/search/utils/types.ts b/src/legacy/core_plugins/data/public/search/utils/types.ts index 305f27a86b398c..e0afe99aa81fac 100644 --- a/src/legacy/core_plugins/data/public/search/utils/types.ts +++ b/src/legacy/core_plugins/data/public/search/utils/types.ts @@ -31,3 +31,9 @@ export interface RequestInspectorStats { hits?: InspectorStat; requestTime?: InspectorStat; } + +export interface AggResponseBucket { + key_as_string: string; + key: number; + doc_count: number; +} diff --git a/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts b/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts index b0bb17ce1ac7f7..91b5c7f13dc954 100644 --- a/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts +++ b/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts @@ -58,8 +58,7 @@ export { stateMonitorFactory } from 'ui/state_management/state_monitor_factory'; export { subscribeWithScope } from 'ui/utils/subscribe_with_scope'; // @ts-ignore export { timezoneProvider } from 'ui/vis/lib/timezone'; -// @ts-ignore -export { tabifyAggResponse } from 'ui/agg_response/tabify'; +export { tabifyAggResponse } from '../../../data/public'; export { unhashUrl } from '../../../../../plugins/kibana_utils/public'; export { migrateLegacyQuery, diff --git a/src/legacy/core_plugins/vis_type_table/public/agg_table/__tests__/agg_table.js b/src/legacy/core_plugins/vis_type_table/public/agg_table/__tests__/agg_table.js index 0dbff60613cb08..9fe7920588cd24 100644 --- a/src/legacy/core_plugins/vis_type_table/public/agg_table/__tests__/agg_table.js +++ b/src/legacy/core_plugins/vis_type_table/public/agg_table/__tests__/agg_table.js @@ -21,7 +21,11 @@ import $ from 'jquery'; import moment from 'moment'; import ngMock from 'ng_mock'; import expect from '@kbn/expect'; -import fixtures from 'fixtures/fake_hierarchical_data'; +import { + metricOnly, + threeTermBuckets, + oneTermOneHistogramBucketWithTwoMetricsOneTopHitOneDerivative, +} from 'fixtures/fake_hierarchical_data'; import sinon from 'sinon'; import { tabifyAggResponse, npStart } from '../../legacy_imports'; import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; @@ -44,7 +48,7 @@ describe('Table Vis - AggTable Directive', function() { const init = () => { const vis1 = new visualizationsStart.Vis(indexPattern, 'table'); - tabifiedData.metricOnly = tabifyAggResponse(vis1.aggs, fixtures.metricOnly); + tabifiedData.metricOnly = tabifyAggResponse(vis1.aggs, metricOnly); const vis2 = new visualizationsStart.Vis(indexPattern, { type: 'table', @@ -61,7 +65,7 @@ describe('Table Vis - AggTable Directive', function() { vis2.aggs.aggs.forEach(function(agg, i) { agg.id = 'agg_' + (i + 1); }); - tabifiedData.threeTermBuckets = tabifyAggResponse(vis2.aggs, fixtures.threeTermBuckets, { + tabifiedData.threeTermBuckets = tabifyAggResponse(vis2.aggs, threeTermBuckets, { metricsAtAllLevels: true, }); @@ -94,7 +98,7 @@ describe('Table Vis - AggTable Directive', function() { tabifiedData.oneTermOneHistogramBucketWithTwoMetricsOneTopHitOneDerivative = tabifyAggResponse( vis3.aggs, - fixtures.oneTermOneHistogramBucketWithTwoMetricsOneTopHitOneDerivative + oneTermOneHistogramBucketWithTwoMetricsOneTopHitOneDerivative ); }; diff --git a/src/legacy/core_plugins/vis_type_table/public/agg_table/__tests__/agg_table_group.js b/src/legacy/core_plugins/vis_type_table/public/agg_table/__tests__/agg_table_group.js index f6ae41b024b7de..79d4d7c40d3559 100644 --- a/src/legacy/core_plugins/vis_type_table/public/agg_table/__tests__/agg_table_group.js +++ b/src/legacy/core_plugins/vis_type_table/public/agg_table/__tests__/agg_table_group.js @@ -20,7 +20,7 @@ import $ from 'jquery'; import ngMock from 'ng_mock'; import expect from '@kbn/expect'; -import fixtures from 'fixtures/fake_hierarchical_data'; +import { metricOnly, threeTermBuckets } from 'fixtures/fake_hierarchical_data'; import { tabifyAggResponse, npStart } from '../../legacy_imports'; import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; import { getAngularModule } from '../../get_inner_angular'; @@ -36,7 +36,7 @@ describe('Table Vis - AggTableGroup Directive', function() { const init = () => { const vis1 = new visualizationsStart.Vis(indexPattern, 'table'); - tabifiedData.metricOnly = tabifyAggResponse(vis1.aggs, fixtures.metricOnly); + tabifiedData.metricOnly = tabifyAggResponse(vis1.aggs, metricOnly); const vis2 = new visualizationsStart.Vis(indexPattern, { type: 'pie', @@ -50,7 +50,7 @@ describe('Table Vis - AggTableGroup Directive', function() { vis2.aggs.aggs.forEach(function(agg, i) { agg.id = 'agg_' + (i + 1); }); - tabifiedData.threeTermBuckets = tabifyAggResponse(vis2.aggs, fixtures.threeTermBuckets); + tabifiedData.threeTermBuckets = tabifyAggResponse(vis2.aggs, threeTermBuckets); }; const initLocalAngular = () => { diff --git a/src/legacy/core_plugins/vis_type_table/public/legacy_imports.ts b/src/legacy/core_plugins/vis_type_table/public/legacy_imports.ts index cb44814897bcfc..90929150de9c39 100644 --- a/src/legacy/core_plugins/vis_type_table/public/legacy_imports.ts +++ b/src/legacy/core_plugins/vis_type_table/public/legacy_imports.ts @@ -24,9 +24,7 @@ export { IAggConfig, AggGroupNames, Schemas } from 'ui/agg_types'; export { PaginateDirectiveProvider } from 'ui/directives/paginate'; // @ts-ignore export { PaginateControlsDirectiveProvider } from 'ui/directives/paginate'; -export { tabifyGetColumns } from 'ui/agg_response/tabify/_get_columns'; -// @ts-ignore -export { tabifyAggResponse } from 'ui/agg_response/tabify'; +export { tabifyAggResponse, tabifyGetColumns } from '../../data/public'; export { configureAppAngularModule, KbnAccessibleClickProvider, diff --git a/src/legacy/core_plugins/vis_type_vislib/public/legacy_imports.ts b/src/legacy/core_plugins/vis_type_vislib/public/legacy_imports.ts index 9c79be98a320c3..1c8e679f7d61f3 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/legacy_imports.ts +++ b/src/legacy/core_plugins/vis_type_vislib/public/legacy_imports.ts @@ -19,10 +19,8 @@ export { AggType, AggGroupNames, IAggConfig, IAggType, Schemas } from 'ui/agg_types'; export { getFormat } from 'ui/visualize/loader/pipeline_helpers/utilities'; -// @ts-ignore -export { tabifyAggResponse } from 'ui/agg_response/tabify'; +export { tabifyAggResponse, tabifyGetColumns } from '../../data/public'; // @ts-ignore export { buildHierarchicalData } from 'ui/agg_response/hierarchical/build_hierarchical_data'; // @ts-ignore export { buildPointSeriesData } from 'ui/agg_response/point_series/point_series'; -export { tabifyGetColumns } from '../../../ui/public/agg_response/tabify/_get_columns'; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/pie_chart.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/pie_chart.js index 534a5231037745..9c9c5a84f046cc 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/pie_chart.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/pie_chart.js @@ -22,7 +22,7 @@ import _ from 'lodash'; import $ from 'jquery'; import expect from '@kbn/expect'; -import fixtures from 'fixtures/fake_hierarchical_data'; +import { threeTermBuckets } from 'fixtures/fake_hierarchical_data'; import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; import { start as visualizationsStart } from '../../../../../visualizations/public/np_ready/public/legacy'; @@ -147,7 +147,7 @@ describe('No global chart settings', function() { }); beforeEach(async () => { - const table1 = tabifyAggResponse(stubVis1.aggs, fixtures.threeTermBuckets, { + const table1 = tabifyAggResponse(stubVis1.aggs, threeTermBuckets, { metricsAtAllLevels: true, }); data1 = await responseHandler(table1, rowAggDimensions); @@ -234,7 +234,7 @@ describe('Vislib PieChart Class Test Suite', function() { }); beforeEach(async () => { - const table = tabifyAggResponse(stubVis.aggs, fixtures.threeTermBuckets, { + const table = tabifyAggResponse(stubVis.aggs, threeTermBuckets, { metricsAtAllLevels: true, }); data = await responseHandler(table, dataDimensions); diff --git a/src/legacy/ui/public/agg_response/index.js b/src/legacy/ui/public/agg_response/index.js index 41d45d1a06ca43..139a124356de21 100644 --- a/src/legacy/ui/public/agg_response/index.js +++ b/src/legacy/ui/public/agg_response/index.js @@ -19,7 +19,7 @@ import { buildHierarchicalData } from './hierarchical/build_hierarchical_data'; import { buildPointSeriesData } from './point_series/point_series'; -import { tabifyAggResponse } from './tabify/tabify'; +import { tabifyAggResponse } from '../../../core_plugins/data/public'; export const aggResponseIndex = { hierarchical: buildHierarchicalData, diff --git a/src/legacy/ui/public/agg_response/tabify/__tests__/_get_columns.js b/src/legacy/ui/public/agg_response/tabify/__tests__/_get_columns.js deleted file mode 100644 index 3eb41c03050d03..00000000000000 --- a/src/legacy/ui/public/agg_response/tabify/__tests__/_get_columns.js +++ /dev/null @@ -1,199 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import expect from '@kbn/expect'; -import ngMock from 'ng_mock'; -import { tabifyGetColumns } from '../_get_columns'; -import { start as visualizationsStart } from '../../../../../core_plugins/visualizations/public/np_ready/public/legacy'; -import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; -describe('get columns', function() { - let indexPattern; - - beforeEach(ngMock.module('kibana')); - beforeEach( - ngMock.inject(function(Private) { - indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); - }) - ); - - it('should inject a count metric if no aggs exist', function() { - const vis = new visualizationsStart.Vis(indexPattern, { - type: 'pie', - }); - while (vis.aggs.length) vis.aggs.pop(); - const columns = tabifyGetColumns( - vis.getAggConfig().getResponseAggs(), - null, - vis.isHierarchical() - ); - - expect(columns).to.have.length(1); - expect(columns[0]).to.have.property('aggConfig'); - expect(columns[0].aggConfig.type).to.have.property('name', 'count'); - }); - - it('should inject a count metric if only buckets exist', function() { - const vis = new visualizationsStart.Vis(indexPattern, { - type: 'pie', - aggs: [ - { - type: 'date_histogram', - schema: 'segment', - params: { field: '@timestamp', interval: '10s' }, - }, - ], - }); - - const columns = tabifyGetColumns(vis.getAggConfig().getResponseAggs(), !vis.isHierarchical()); - - expect(columns).to.have.length(2); - expect(columns[1]).to.have.property('aggConfig'); - expect(columns[1].aggConfig.type).to.have.property('name', 'count'); - }); - - it('should inject the metric after each bucket if the vis is hierarchical', function() { - const vis = new visualizationsStart.Vis(indexPattern, { - type: 'pie', - aggs: [ - { - type: 'date_histogram', - schema: 'segment', - params: { field: '@timestamp', interval: '10s' }, - }, - { - type: 'date_histogram', - schema: 'segment', - params: { field: '@timestamp', interval: '10s' }, - }, - { - type: 'date_histogram', - schema: 'segment', - params: { field: '@timestamp', interval: '10s' }, - }, - { - type: 'date_histogram', - schema: 'segment', - params: { field: '@timestamp', interval: '10s' }, - }, - ], - }); - - const columns = tabifyGetColumns(vis.getAggConfig().getResponseAggs(), !vis.isHierarchical()); - - expect(columns).to.have.length(8); - columns.forEach(function(column, i) { - expect(column).to.have.property('aggConfig'); - expect(column.aggConfig.type).to.have.property('name', i % 2 ? 'count' : 'date_histogram'); - }); - }); - - it('should inject the multiple metrics after each bucket if the vis is hierarchical', function() { - const vis = new visualizationsStart.Vis(indexPattern, { - type: 'pie', - aggs: [ - { - type: 'date_histogram', - schema: 'segment', - params: { field: '@timestamp', interval: '10s' }, - }, - { type: 'avg', schema: 'metric', params: { field: 'bytes' } }, - { - type: 'date_histogram', - schema: 'segment', - params: { field: '@timestamp', interval: '10s' }, - }, - { - type: 'date_histogram', - schema: 'segment', - params: { field: '@timestamp', interval: '10s' }, - }, - { type: 'sum', schema: 'metric', params: { field: 'bytes' } }, - { - type: 'date_histogram', - schema: 'segment', - params: { field: '@timestamp', interval: '10s' }, - }, - ], - }); - - const columns = tabifyGetColumns(vis.getAggConfig().getResponseAggs(), !vis.isHierarchical()); - - function checkColumns(column, i) { - expect(column).to.have.property('aggConfig'); - switch (i) { - case 0: - expect(column.aggConfig.type).to.have.property('name', 'date_histogram'); - break; - case 1: - expect(column.aggConfig.type).to.have.property('name', 'avg'); - break; - case 2: - expect(column.aggConfig.type).to.have.property('name', 'sum'); - break; - } - } - - expect(columns).to.have.length(12); - for (let i = 0; i < columns.length; i += 3) { - columns.slice(i, i + 3).forEach(checkColumns); - } - }); - - it('should put all metrics at the end of the columns if the vis is not hierarchical', function() { - const vis = new visualizationsStart.Vis(indexPattern, { - type: 'histogram', - aggs: [ - { - type: 'date_histogram', - schema: 'segment', - params: { field: '@timestamp', interval: '10s' }, - }, - { type: 'avg', schema: 'metric', params: { field: 'bytes' } }, - { - type: 'date_histogram', - schema: 'segment', - params: { field: '@timestamp', interval: '10s' }, - }, - { - type: 'date_histogram', - schema: 'segment', - params: { field: '@timestamp', interval: '10s' }, - }, - { type: 'sum', schema: 'metric', params: { field: 'bytes' } }, - { - type: 'date_histogram', - schema: 'segment', - params: { field: '@timestamp', interval: '10s' }, - }, - ], - }); - - const columns = tabifyGetColumns(vis.getAggConfig().getResponseAggs(), !vis.isHierarchical()); - expect(columns).to.have.length(6); - - // sum should be last - expect(columns.pop().aggConfig.type).to.have.property('name', 'sum'); - // avg should be before that - expect(columns.pop().aggConfig.type).to.have.property('name', 'avg'); - // the rest are date_histograms - while (columns.length) { - expect(columns.pop().aggConfig.type).to.have.property('name', 'date_histogram'); - } - }); -}); diff --git a/src/legacy/ui/public/agg_response/tabify/__tests__/_integration.js b/src/legacy/ui/public/agg_response/tabify/__tests__/_integration.js deleted file mode 100644 index f3f2e20149acfc..00000000000000 --- a/src/legacy/ui/public/agg_response/tabify/__tests__/_integration.js +++ /dev/null @@ -1,175 +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 fixtures from 'fixtures/fake_hierarchical_data'; -import expect from '@kbn/expect'; -import ngMock from 'ng_mock'; -import { tabifyAggResponse } from '../tabify'; -import { start as visualizationsStart } from '../../../../../core_plugins/visualizations/public/np_ready/public/legacy'; -import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; - -describe('tabifyAggResponse Integration', function() { - let indexPattern; - - beforeEach(ngMock.module('kibana')); - beforeEach( - ngMock.inject(function(Private) { - indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); - }) - ); - - function normalizeIds(vis) { - vis.aggs.aggs.forEach(function(agg, i) { - agg.id = 'agg_' + (i + 1); - }); - } - - it('transforms a simple response properly', function() { - const vis = new visualizationsStart.Vis(indexPattern, { - type: 'histogram', - aggs: [], - }); - normalizeIds(vis); - - const resp = tabifyAggResponse(vis.getAggConfig(), fixtures.metricOnly, { - metricsAtAllLevels: vis.isHierarchical(), - }); - - expect(resp) - .to.have.property('rows') - .and.property('columns'); - expect(resp.rows).to.have.length(1); - expect(resp.columns).to.have.length(1); - - expect(resp.rows[0]).to.eql({ 'col-0-agg_1': 1000 }); - expect(resp.columns[0]).to.have.property('aggConfig', vis.aggs[0]); - }); - - describe('transforms a complex response', function() { - this.slow(1000); - - let vis; - let avg; - let ext; - let src; - let os; - let esResp; - - beforeEach(function() { - vis = new visualizationsStart.Vis(indexPattern, { - type: 'pie', - aggs: [ - { type: 'avg', schema: 'metric', params: { field: 'bytes' } }, - { type: 'terms', schema: 'split', params: { field: 'extension' } }, - { type: 'terms', schema: 'segment', params: { field: 'geo.src' } }, - { type: 'terms', schema: 'segment', params: { field: 'machine.os' } }, - ], - }); - normalizeIds(vis); - - avg = vis.aggs[0]; - ext = vis.aggs[1]; - src = vis.aggs[2]; - os = vis.aggs[3]; - - esResp = _.cloneDeep(fixtures.threeTermBuckets); - // remove the buckets for css in MX - esResp.aggregations.agg_2.buckets[1].agg_3.buckets[0].agg_4.buckets = []; - }); - - // check that the columns of a table are formed properly - function expectColumns(table, aggs) { - expect(table.columns) - .to.be.an('array') - .and.have.length(aggs.length); - aggs.forEach(function(agg, i) { - expect(table.columns[i]).to.have.property('aggConfig', agg); - }); - } - - // check that a row has expected values - function expectRow(row, asserts) { - expect(row).to.be.an('object'); - asserts.forEach(function(assert, i) { - if (row[`col-${i}`]) { - assert(row[`col-${i}`]); - } - }); - } - - // check for two character country code - function expectCountry(val) { - expect(val).to.be.a('string'); - expect(val).to.have.length(2); - } - - // check for an OS term - function expectExtension(val) { - expect(val).to.match(/^(js|png|html|css|jpg)$/); - } - - // check for an OS term - function expectOS(val) { - expect(val).to.match(/^(win|mac|linux)$/); - } - - // check for something like an average bytes result - function expectAvgBytes(val) { - expect(val).to.be.a('number'); - expect(val === 0 || val > 1000).to.be.ok(); - } - - it('for non-hierarchical vis', function() { - // the default for a non-hierarchical vis is to display - // only complete rows, and only put the metrics at the end. - - const tabbed = tabifyAggResponse(vis.getAggConfig(), esResp, { metricsAtAllLevels: false }); - - expectColumns(tabbed, [ext, src, os, avg]); - - tabbed.rows.forEach(function(row) { - expectRow(row, [expectExtension, expectCountry, expectOS, expectAvgBytes]); - }); - }); - - it('for hierarchical vis', function() { - // since we have partialRows we expect that one row will have some empty - // values, and since the vis is hierarchical and we are NOT using - // minimalColumns we should expect the partial row to be completely after - // the existing bucket and it's metric - - vis.isHierarchical = _.constant(true); - const tabbed = tabifyAggResponse(vis.getAggConfig(), esResp, { metricsAtAllLevels: true }); - - expectColumns(tabbed, [ext, avg, src, avg, os, avg]); - - tabbed.rows.forEach(function(row) { - expectRow(row, [ - expectExtension, - expectAvgBytes, - expectCountry, - expectAvgBytes, - expectOS, - expectAvgBytes, - ]); - }); - }); - }); -}); diff --git a/src/legacy/ui/public/agg_response/tabify/__tests__/_response_writer.js b/src/legacy/ui/public/agg_response/tabify/__tests__/_response_writer.js deleted file mode 100644 index b0c0f2f3d91002..00000000000000 --- a/src/legacy/ui/public/agg_response/tabify/__tests__/_response_writer.js +++ /dev/null @@ -1,186 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import expect from '@kbn/expect'; -import ngMock from 'ng_mock'; -import { TabbedAggResponseWriter } from '../_response_writer'; -import { start as visualizationsStart } from '../../../../../core_plugins/visualizations/public/np_ready/public/legacy'; -import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; - -describe('TabbedAggResponseWriter class', function() { - let Private; - let indexPattern; - - beforeEach(ngMock.module('kibana')); - beforeEach( - ngMock.inject(function($injector) { - Private = $injector.get('Private'); - - indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); - }) - ); - - const splitAggConfig = [ - { - type: 'terms', - params: { - field: 'geo.src', - }, - }, - ]; - - const twoSplitsAggConfig = [ - { - type: 'terms', - params: { - field: 'geo.src', - }, - }, - { - type: 'terms', - params: { - field: 'machine.os.raw', - }, - }, - ]; - - const createResponseWritter = (aggs = [], opts = {}) => { - const vis = new visualizationsStart.Vis(indexPattern, { type: 'histogram', aggs: aggs }); - return new TabbedAggResponseWriter(vis.getAggConfig(), opts); - }; - - describe('Constructor', function() { - let responseWriter; - beforeEach(() => { - responseWriter = createResponseWritter(twoSplitsAggConfig); - }); - - it('creates aggStack', () => { - expect(responseWriter.aggStack.length).to.eql(3); - }); - - it('generates columns', () => { - expect(responseWriter.columns.length).to.eql(3); - }); - - it('correctly generates columns with metricsAtAllLevels set to true', () => { - const minimalColumnsResponseWriter = createResponseWritter(twoSplitsAggConfig, { - metricsAtAllLevels: true, - }); - expect(minimalColumnsResponseWriter.columns.length).to.eql(4); - }); - - describe('sets timeRange', function() { - it("to the first nested object's range", function() { - const vis = new visualizationsStart.Vis(indexPattern, { type: 'histogram', aggs: [] }); - const range = { - gte: 0, - lte: 100, - }; - - const writer = new TabbedAggResponseWriter(vis.getAggConfig(), { - timeRange: { - '@timestamp': range, - }, - }); - - expect(writer.timeRange.gte).to.be(range.gte); - expect(writer.timeRange.lte).to.be(range.lte); - expect(writer.timeRange.name).to.be('@timestamp'); - }); - - it('to undefined if no nested object', function() { - const vis = new visualizationsStart.Vis(indexPattern, { type: 'histogram', aggs: [] }); - - const writer = new TabbedAggResponseWriter(vis.getAggConfig(), { - timeRange: {}, - }); - expect(writer).to.have.property('timeRange', undefined); - }); - }); - }); - - describe('row()', function() { - let responseWriter; - - beforeEach(() => { - responseWriter = createResponseWritter(splitAggConfig, { partialRows: true }); - }); - - it('adds the row to the array', () => { - responseWriter.rowBuffer['col-0'] = 'US'; - responseWriter.rowBuffer['col-1'] = 5; - responseWriter.row(); - expect(responseWriter.rows.length).to.eql(1); - expect(responseWriter.rows[0]).to.eql({ 'col-0': 'US', 'col-1': 5 }); - }); - - it('correctly handles bucketBuffer', () => { - responseWriter.bucketBuffer.push({ id: 'col-0', value: 'US' }); - responseWriter.rowBuffer['col-1'] = 5; - responseWriter.row(); - expect(responseWriter.rows.length).to.eql(1); - expect(responseWriter.rows[0]).to.eql({ 'col-0': 'US', 'col-1': 5 }); - }); - - it("doesn't add an empty row", () => { - responseWriter.row(); - expect(responseWriter.rows.length).to.eql(0); - }); - }); - - describe('response()', () => { - let responseWriter; - - beforeEach(() => { - responseWriter = createResponseWritter(splitAggConfig); - }); - - it('produces correct response', () => { - responseWriter.rowBuffer['col-0-1'] = 'US'; - responseWriter.rowBuffer['col-1-2'] = 5; - responseWriter.row(); - const response = responseWriter.response(); - expect(response).to.have.property('rows'); - expect(response.rows).to.eql([{ 'col-0-1': 'US', 'col-1-2': 5 }]); - expect(response).to.have.property('columns'); - expect(response.columns.length).to.equal(2); - expect(response.columns[0]).to.have.property('id', 'col-0-1'); - expect(response.columns[0]).to.have.property('name', 'geo.src: Descending'); - expect(response.columns[0]).to.have.property('aggConfig'); - expect(response.columns[1]).to.have.property('id', 'col-1-2'); - expect(response.columns[1]).to.have.property('name', 'Count'); - expect(response.columns[1]).to.have.property('aggConfig'); - }); - - it('produces correct response for no data', () => { - const response = responseWriter.response(); - expect(response).to.have.property('rows'); - expect(response.rows.length).to.be(0); - expect(response).to.have.property('columns'); - expect(response.columns.length).to.equal(2); - expect(response.columns[0]).to.have.property('id', 'col-0-1'); - expect(response.columns[0]).to.have.property('name', 'geo.src: Descending'); - expect(response.columns[0]).to.have.property('aggConfig'); - expect(response.columns[1]).to.have.property('id', 'col-1-2'); - expect(response.columns[1]).to.have.property('name', 'Count'); - expect(response.columns[1]).to.have.property('aggConfig'); - }); - }); -}); diff --git a/src/legacy/ui/public/agg_response/tabify/_buckets.js b/src/legacy/ui/public/agg_response/tabify/_buckets.js deleted file mode 100644 index 7180a056ab0ca0..00000000000000 --- a/src/legacy/ui/public/agg_response/tabify/_buckets.js +++ /dev/null @@ -1,123 +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 moment from 'moment'; - -function TabifyBuckets(aggResp, aggParams, timeRange) { - if (_.has(aggResp, 'buckets')) { - this.buckets = aggResp.buckets; - } else if (aggResp) { - // Some Bucket Aggs only return a single bucket (like filter). - // In those instances, the aggResp is the content of the single bucket. - this.buckets = [aggResp]; - } else { - this.buckets = []; - } - - this.objectMode = _.isPlainObject(this.buckets); - if (this.objectMode) { - this._keys = _.keys(this.buckets); - this.length = this._keys.length; - } else { - this.length = this.buckets.length; - } - - if (this.length && aggParams) { - this._orderBucketsAccordingToParams(aggParams); - if (aggParams.drop_partials) { - this._dropPartials(aggParams, timeRange); - } - } -} - -TabifyBuckets.prototype.forEach = function(fn) { - const buckets = this.buckets; - - if (this.objectMode) { - this._keys.forEach(function(key) { - fn(buckets[key], key); - }); - } else { - buckets.forEach(function(bucket) { - fn(bucket, bucket.key); - }); - } -}; - -TabifyBuckets.prototype._isRangeEqual = function(range1, range2) { - return ( - _.get(range1, 'from', null) === _.get(range2, 'from', null) && - _.get(range1, 'to', null) === _.get(range2, 'to', null) - ); -}; - -TabifyBuckets.prototype._orderBucketsAccordingToParams = function(params) { - if (params.filters && this.objectMode) { - this._keys = params.filters.map(filter => { - const query = _.get(filter, 'input.query.query_string.query', filter.input.query); - const queryString = typeof query === 'string' ? query : JSON.stringify(query); - return filter.label || queryString || '*'; - }); - } else if (params.ranges && this.objectMode) { - this._keys = params.ranges.map(range => { - return _.findKey(this.buckets, el => this._isRangeEqual(el, range)); - }); - } else if (params.ranges && params.field.type !== 'date') { - let ranges = params.ranges; - if (params.ipRangeType) { - ranges = params.ipRangeType === 'mask' ? ranges.mask : ranges.fromTo; - } - this.buckets = ranges.map(range => { - if (range.mask) { - return this.buckets.find(el => el.key === range.mask); - } - return this.buckets.find(el => this._isRangeEqual(el, range)); - }); - } -}; - -// dropPartials should only be called if the aggParam setting is enabled, -// and the agg field is the same as the Time Range. -TabifyBuckets.prototype._dropPartials = function(params, timeRange) { - if ( - !timeRange || - this.buckets.length <= 1 || - this.objectMode || - params.field.name !== timeRange.name - ) { - return; - } - - const interval = this.buckets[1].key - this.buckets[0].key; - - this.buckets = this.buckets.filter(bucket => { - if (moment(bucket.key).isBefore(timeRange.gte)) { - return false; - } - if (moment(bucket.key + interval).isAfter(timeRange.lte)) { - return false; - } - return true; - }); - - this.length = this.buckets.length; -}; - -export { TabifyBuckets }; diff --git a/src/legacy/ui/public/agg_response/tabify/_response_writer.js b/src/legacy/ui/public/agg_response/tabify/_response_writer.js deleted file mode 100644 index 85586c7ca7fdaf..00000000000000 --- a/src/legacy/ui/public/agg_response/tabify/_response_writer.js +++ /dev/null @@ -1,97 +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 { toArray } from 'lodash'; -import { tabifyGetColumns } from './_get_columns'; - -/** - * Writer class that collects information about an aggregation response and - * produces a table, or a series of tables. - * - * @param {AggConfigs} aggs - the agg configs object to which the aggregation response correlates - * @param {boolean} metricsAtAllLevels - setting to true will produce metrics for every bucket - * @param {boolean} partialRows - setting to true will not remove rows with missing values - * @param {Object} timeRange - time range object, if provided - */ -function TabbedAggResponseWriter( - aggs, - { metricsAtAllLevels = false, partialRows = false, timeRange } = {} -) { - // Private - this._removePartialRows = !partialRows; - - // Public - this.rowBuffer = {}; - this.bucketBuffer = []; - this.metricBuffer = []; - this.aggs = aggs; - this.partialRows = partialRows; - this.columns = tabifyGetColumns(aggs.getResponseAggs(), !metricsAtAllLevels); - this.aggStack = [...this.columns]; - this.rows = []; - // Extract the time range object if provided - if (timeRange) { - const timeRangeKey = Object.keys(timeRange)[0]; - this.timeRange = timeRange[timeRangeKey]; - if (this.timeRange) { - this.timeRange.name = timeRangeKey; - } - } -} - -TabbedAggResponseWriter.prototype.isPartialRow = function(row) { - return !this.columns.map(column => row.hasOwnProperty(column.id)).every(c => c === true); -}; - -/** - * Create a new row by reading the row buffer and bucketBuffer - */ -TabbedAggResponseWriter.prototype.row = function() { - this.bucketBuffer.forEach(bucket => { - this.rowBuffer[bucket.id] = bucket.value; - }); - - this.metricBuffer.forEach(metric => { - this.rowBuffer[metric.id] = metric.value; - }); - - if ( - !toArray(this.rowBuffer).length || - (this._removePartialRows && this.isPartialRow(this.rowBuffer)) - ) { - return; - } - - this.rows.push(this.rowBuffer); - this.rowBuffer = {}; -}; - -/** - * Get the actual response - * - * @return {object} - the final table - */ -TabbedAggResponseWriter.prototype.response = function() { - return { - columns: this.columns, - rows: this.rows, - }; -}; - -export { TabbedAggResponseWriter }; diff --git a/src/legacy/ui/public/agg_response/tabify/tabify.js b/src/legacy/ui/public/agg_response/tabify/tabify.js deleted file mode 100644 index 8316055cb15cc9..00000000000000 --- a/src/legacy/ui/public/agg_response/tabify/tabify.js +++ /dev/null @@ -1,134 +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 { TabbedAggResponseWriter } from './_response_writer'; -import { TabifyBuckets } from './_buckets'; - -/** - * Sets up the ResponseWriter and kicks off bucket collection. - * - * @param {AggConfigs} aggs - the agg configs object to which the aggregation response correlates - * @param {Object} esResponse - response that came back from Elasticsearch - * @param {Object} respOpts - options object for the ResponseWriter with params set by Courier - * @param {boolean} respOpts.metricsAtAllLevels - setting to true will produce metrics for every bucket - * @param {boolean} respOpts.partialRows - setting to true will not remove rows with missing values - * @param {Object} respOpts.timeRange - time range object, if provided - */ -export function tabifyAggResponse(aggs, esResponse, respOpts = {}) { - const write = new TabbedAggResponseWriter(aggs, respOpts); - - const topLevelBucket = _.assign({}, esResponse.aggregations, { - doc_count: esResponse.hits.total, - }); - - collectBucket(write, topLevelBucket, '', 1); - - return write.response(); -} - -/** - * read an aggregation from a bucket, which *might* be found at key (if - * the response came in object form), and will recurse down the aggregation - * tree and will pass the read values to the ResponseWriter. - * - * @param {object} bucket - a bucket from the aggResponse - * @param {undefined|string} key - the key where the bucket was found - * @returns {undefined} - */ -function collectBucket(write, bucket, key, aggScale) { - const column = write.aggStack.shift(); - const agg = column.aggConfig; - const aggInfo = agg.write(write.aggs); - aggScale *= aggInfo.metricScale || 1; - - switch (agg.type.type) { - case 'buckets': - const buckets = new TabifyBuckets(bucket[agg.id], agg.params, write.timeRange); - if (buckets.length) { - buckets.forEach(function(subBucket, key) { - // if the bucket doesn't have value don't add it to the row - // we don't want rows like: { column1: undefined, column2: 10 } - const bucketValue = agg.getKey(subBucket, key); - const hasBucketValue = typeof bucketValue !== 'undefined'; - if (hasBucketValue) { - write.bucketBuffer.push({ id: column.id, value: bucketValue }); - } - collectBucket(write, subBucket, agg.getKey(subBucket, key), aggScale); - if (hasBucketValue) { - write.bucketBuffer.pop(); - } - }); - } else if (write.partialRows) { - // we don't have any buckets, but we do have metrics at this - // level, then pass all the empty buckets and jump back in for - // the metrics. - write.aggStack.unshift(column); - passEmptyBuckets(write, bucket, key, aggScale); - write.aggStack.shift(); - } else { - // we don't have any buckets, and we don't have isHierarchical - // data, so no metrics, just try to write the row - write.row(); - } - break; - case 'metrics': - let value = agg.getValue(bucket); - // since the aggregation could be a non integer (such as a max date) - // only do the scaling calculation if it is needed. - if (aggScale !== 1) { - value *= aggScale; - } - write.metricBuffer.push({ id: column.id, value: value }); - - if (!write.aggStack.length) { - // row complete - write.row(); - } else { - // process the next agg at this same level - collectBucket(write, bucket, key, aggScale); - } - - write.metricBuffer.pop(); - - break; - } - - write.aggStack.unshift(column); -} - -// write empty values for each bucket agg, then write -// the metrics from the initial bucket using collectBucket() -function passEmptyBuckets(write, bucket, key, aggScale) { - const column = write.aggStack.shift(); - const agg = column.aggConfig; - - switch (agg.type.type) { - case 'metrics': - // pass control back to collectBucket() - write.aggStack.unshift(column); - collectBucket(write, bucket, key, aggScale); - return; - - case 'buckets': - passEmptyBuckets(write, bucket, key, aggScale); - } - - write.aggStack.unshift(column); -} diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.js index cb8b43a6c312b2..0912e5a9f1283d 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.js @@ -11,7 +11,7 @@ import { VECTOR_SHAPE_TYPES } from '../vector_feature_types'; import { HeatmapLayer } from '../../heatmap_layer'; import { VectorLayer } from '../../vector_layer'; import { AggConfigs, Schemas } from 'ui/agg_types'; -import { tabifyAggResponse } from 'ui/agg_response/tabify'; +import { tabifyAggResponse } from '../../../../../../../../src/legacy/core_plugins/data/public'; import { convertToGeoJson } from './convert_to_geojson'; import { VectorStyle } from '../../styles/vector/vector_style'; import { From 39c835af605e9c832cde2d2a40200ef8283a46f8 Mon Sep 17 00:00:00 2001 From: Sebastian Grodzicki Date: Thu, 20 Feb 2020 14:05:21 +0100 Subject: [PATCH 7/9] Add owners for monitoring plugin (#57987) --- .github/CODEOWNERS | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 56db8d3793f57c..bea10a1c8b31c8 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -75,6 +75,7 @@ /x-pack/plugins/ingest_manager/ @elastic/ingest /x-pack/legacy/plugins/ingest_manager/ @elastic/ingest /x-pack/plugins/observability/ @elastic/logs-metrics-ui @elastic/apm-ui @elastic/uptime @elastic/ingest +/x-pack/legacy/plugins/monitoring/ @elastic/stack-monitoring-ui # Machine Learning /x-pack/legacy/plugins/ml/ @elastic/ml-ui From 47bb7c61b67c1370a73fe247f4248326b8f696b4 Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Thu, 20 Feb 2020 09:13:49 -0500 Subject: [PATCH 8/9] update estimateBucketSpan schema and api test (#58004) --- .../new_platform/job_validation_schema.ts | 1 + .../apis/ml/bucket_span_estimator.ts | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/x-pack/legacy/plugins/ml/server/new_platform/job_validation_schema.ts b/x-pack/legacy/plugins/ml/server/new_platform/job_validation_schema.ts index 1cc6e8a97ffc05..5917ec50884d80 100644 --- a/x-pack/legacy/plugins/ml/server/new_platform/job_validation_schema.ts +++ b/x-pack/legacy/plugins/ml/server/new_platform/job_validation_schema.ts @@ -11,6 +11,7 @@ export const estimateBucketSpanSchema = schema.object({ aggTypes: schema.arrayOf(schema.nullable(schema.string())), duration: schema.object({ start: schema.number(), end: schema.number() }), fields: schema.arrayOf(schema.nullable(schema.string())), + filters: schema.maybe(schema.arrayOf(schema.any())), index: schema.string(), query: schema.any(), splitField: schema.maybe(schema.string()), diff --git a/x-pack/test/api_integration/apis/ml/bucket_span_estimator.ts b/x-pack/test/api_integration/apis/ml/bucket_span_estimator.ts index 47afe7553fe62b..1c7245234b089f 100644 --- a/x-pack/test/api_integration/apis/ml/bucket_span_estimator.ts +++ b/x-pack/test/api_integration/apis/ml/bucket_span_estimator.ts @@ -69,6 +69,23 @@ export default ({ getService }: FtrProviderContext) => { responseBody: { name: '3h', ms: 10800000 }, }, }, + { + testTitleSuffix: 'with 1 field, 1 agg, no split, and empty filters', + user: USER.ML_POWERUSER, + requestBody: { + aggTypes: ['avg'], + duration: { start: 1560297859000, end: 1562975136000 }, + fields: ['taxless_total_price'], + filters: [], + index: 'ecommerce', + query: { bool: { must: [{ match_all: {} }] } }, + timeField: 'order_date', + }, + expected: { + responseCode: 200, + responseBody: { name: '15m', ms: 900000 }, + }, + }, ]; describe('bucket span estimator', function() { From 5cfc5ef9fffb3f39fedc06b495aecc881d1b731a Mon Sep 17 00:00:00 2001 From: Michail Yasonik Date: Thu, 20 Feb 2020 09:14:11 -0500 Subject: [PATCH 9/9] Reverting grouped nav UI (#57724) * reverting grouped nav ui * removing Management name change --- docs/management/advanced-options.asciidoc | 3 - src/core/public/chrome/chrome_service.tsx | 1 - .../__snapshots__/nav_drawer.test.tsx.snap | 5283 ----------------- src/core/public/chrome/ui/header/header.tsx | 12 +- src/core/public/chrome/ui/header/index.ts | 1 - .../chrome/ui/header/nav_drawer.test.tsx | 103 - .../public/chrome/ui/header/nav_drawer.tsx | 118 +- src/legacy/core_plugins/kibana/index.js | 2 +- .../home/np_ready/components/home.test.js | 2 +- .../kibana/public/management/index.js | 2 +- .../kibana/ui_setting_defaults.js | 19 - .../ui/public/management/breadcrumbs.ts | 2 +- .../management_sidebar_nav.tsx | 3 +- .../public/legacy/sections_register.js | 4 +- .../management/public/management_app.tsx | 3 +- src/plugins/management/public/plugin.ts | 4 +- .../dashboard/create_and_add_embeddables.js | 1 - .../apps/management/_index_pattern_filter.js | 2 +- test/functional/page_objects/header_page.js | 2 +- test/functional/page_objects/settings_page.ts | 7 - .../core_plugins/application_status.ts | 6 +- .../test_suites/core_plugins/applications.ts | 2 +- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - .../advanced_settings_security.ts | 8 +- .../advanced_settings_spaces.ts | 3 +- .../apps/apm/feature_controls/apm_security.ts | 4 +- .../apps/apm/feature_controls/apm_spaces.ts | 3 +- .../feature_controls/canvas_security.ts | 4 +- .../canvas/feature_controls/canvas_spaces.ts | 3 +- .../feature_controls/dashboard_security.ts | 4 +- .../feature_controls/dashboard_spaces.ts | 9 +- .../dashboard_mode/dashboard_view_mode.js | 9 +- .../feature_controls/dev_tools_security.ts | 4 +- .../feature_controls/dev_tools_spaces.ts | 9 +- .../feature_controls/discover_security.ts | 4 +- .../feature_controls/discover_spaces.ts | 2 - .../graph/feature_controls/graph_security.ts | 4 +- .../graph/feature_controls/graph_spaces.ts | 3 +- .../index_patterns_security.ts | 8 +- .../feature_controls/index_patterns_spaces.ts | 3 +- .../infrastructure_security.ts | 4 +- .../feature_controls/infrastructure_spaces.ts | 9 +- .../infra/feature_controls/logs_security.ts | 4 +- .../infra/feature_controls/logs_spaces.ts | 9 +- .../feature_controls/ml_security.ts | 3 +- .../feature_controls/ml_spaces.ts | 3 +- .../maps/feature_controls/maps_security.ts | 6 +- .../feature_controls/monitoring_security.ts | 3 +- .../feature_controls/monitoring_spaces.ts | 3 +- .../feature_controls/spaces_security.ts | 5 +- .../feature_controls/timelion_security.ts | 4 +- .../feature_controls/timelion_spaces.ts | 9 +- .../feature_controls/uptime_security.ts | 4 +- .../uptime/feature_controls/uptime_spaces.ts | 3 +- .../feature_controls/visualize_security.ts | 4 +- .../feature_controls/visualize_spaces.ts | 9 +- 57 files changed, 79 insertions(+), 5671 deletions(-) delete mode 100644 src/core/public/chrome/ui/header/__snapshots__/nav_drawer.test.tsx.snap delete mode 100644 src/core/public/chrome/ui/header/nav_drawer.test.tsx diff --git a/docs/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc index ec626677d09025..80c9053dc5ae6f 100644 --- a/docs/management/advanced-options.asciidoc +++ b/docs/management/advanced-options.asciidoc @@ -70,9 +70,6 @@ into the document when displaying it. `metrics:max_buckets`:: The maximum numbers of buckets that a single data source can return. This might arise when the user selects a short interval (for example, 1s) for a long time period (1 year). -`pageNavigation`:: The style of navigation menu for Kibana. -Choices are Individual, the legacy style where every plugin is represented in the nav, -and Grouped, a new format that bundles related plugins together in nested navigation. `query:allowLeadingWildcards`:: Allows a wildcard (*) as the first character in a query clause. Only applies when experimental query features are enabled in the query bar. To disallow leading wildcards in Lucene queries, diff --git a/src/core/public/chrome/chrome_service.tsx b/src/core/public/chrome/chrome_service.tsx index 6ab9fe158742a6..2b0b115ce068ee 100644 --- a/src/core/public/chrome/chrome_service.tsx +++ b/src/core/public/chrome/chrome_service.tsx @@ -193,7 +193,6 @@ export class ChromeService { recentlyAccessed$={recentlyAccessed.get$()} navControlsLeft$={navControls.getLeft$()} navControlsRight$={navControls.getRight$()} - navSetting$={uiSettings.get$('pageNavigation')} /> ), diff --git a/src/core/public/chrome/ui/header/__snapshots__/nav_drawer.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/nav_drawer.test.tsx.snap deleted file mode 100644 index cf3b48f237286b..00000000000000 --- a/src/core/public/chrome/ui/header/__snapshots__/nav_drawer.test.tsx.snap +++ /dev/null @@ -1,5283 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`NavDrawer Advanced setting set to grouped renders grouped items 1`] = ` - - - - - - - -`; - -exports[`NavDrawer Advanced setting set to grouped renders individual items if there are less than 7 1`] = ` - - - - - - - -`; - -exports[`NavDrawer Advanced setting set to grouped renders individual items if there is only 1 category 1`] = ` - - - - - - - -`; - -exports[`NavDrawer Advanced setting set to individual renders individual items 1`] = ` - - - - - - - -`; diff --git a/src/core/public/chrome/ui/header/header.tsx b/src/core/public/chrome/ui/header/header.tsx index c3cefd180b16f6..c9a583f39b30cb 100644 --- a/src/core/public/chrome/ui/header/header.tsx +++ b/src/core/public/chrome/ui/header/header.tsx @@ -42,7 +42,7 @@ import { InternalApplicationStart } from '../../../application/types'; import { HttpStart } from '../../../http'; import { ChromeHelpExtension } from '../../chrome_service'; import { HeaderBadge } from './header_badge'; -import { NavSetting, OnIsLockedUpdate } from './'; +import { OnIsLockedUpdate } from './'; import { HeaderBreadcrumbs } from './header_breadcrumbs'; import { HeaderHelpMenu } from './header_help_menu'; import { HeaderNavControls } from './header_nav_controls'; @@ -69,7 +69,6 @@ export interface HeaderProps { navControlsRight$: Rx.Observable; basePath: HttpStart['basePath']; isLocked?: boolean; - navSetting$: Rx.Observable; onIsLockedUpdate?: OnIsLockedUpdate; } @@ -81,7 +80,6 @@ interface State { forceNavigation: boolean; navControlsLeft: readonly ChromeNavControl[]; navControlsRight: readonly ChromeNavControl[]; - navSetting: NavSetting; currentAppId: string | undefined; } @@ -100,7 +98,6 @@ export class Header extends Component { forceNavigation: false, navControlsLeft: [], navControlsRight: [], - navSetting: 'grouped', currentAppId: '', }; } @@ -116,8 +113,7 @@ export class Header extends Component { Rx.combineLatest( this.props.navControlsLeft$, this.props.navControlsRight$, - this.props.application.currentAppId$, - this.props.navSetting$ + this.props.application.currentAppId$ ) ).subscribe({ next: ([ @@ -126,7 +122,7 @@ export class Header extends Component { forceNavigation, navLinks, recentlyAccessed, - [navControlsLeft, navControlsRight, currentAppId, navSetting], + [navControlsLeft, navControlsRight, currentAppId], ]) => { this.setState({ appTitle, @@ -136,7 +132,6 @@ export class Header extends Component { recentlyAccessed, navControlsLeft, navControlsRight, - navSetting, currentAppId, }); }, @@ -225,7 +220,6 @@ export class Header extends Component { void; diff --git a/src/core/public/chrome/ui/header/nav_drawer.test.tsx b/src/core/public/chrome/ui/header/nav_drawer.test.tsx deleted file mode 100644 index 7272935b93a520..00000000000000 --- a/src/core/public/chrome/ui/header/nav_drawer.test.tsx +++ /dev/null @@ -1,103 +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 { cloneDeep } from 'lodash'; -import { mount } from 'enzyme'; -import React from 'react'; -import { NavSetting } from './'; -import { ChromeNavLink } from '../../../'; -import { AppCategory } from 'src/core/types'; -import { DEFAULT_APP_CATEGORIES } from '../../../../utils'; -import { NavDrawer } from './nav_drawer'; -import { euiNavLink } from './nav_link'; - -const { analyze, management, observability, security } = DEFAULT_APP_CATEGORIES; -const mockIBasePath = { - get: () => '/app', - prepend: () => '/app', - remove: () => '/app', -}; - -const getMockProps = (chromeNavLinks: ChromeNavLink[], navSetting: NavSetting = 'grouped') => ({ - navSetting, - navLinks: chromeNavLinks.map(link => - euiNavLink(link, true, undefined, mockIBasePath, () => Promise.resolve()) - ), - chromeNavLinks, - recentlyAccessedItems: [], - basePath: mockIBasePath, -}); - -const makeLink = (id: string, order: number, category?: AppCategory) => ({ - id, - category, - order, - title: id, - baseUrl: `http://localhost:5601/app/${id}`, - legacy: true, -}); - -const getMockChromeNavLink = () => - cloneDeep([ - makeLink('discover', 100, analyze), - makeLink('siem', 500, security), - makeLink('metrics', 600, observability), - makeLink('monitoring', 800, management), - makeLink('visualize', 200, analyze), - makeLink('dashboard', 300, analyze), - makeLink('canvas', 400, { label: 'customCategory' }), - makeLink('logs', 700, observability), - ]); - -describe('NavDrawer', () => { - describe('Advanced setting set to individual', () => { - it('renders individual items', () => { - const component = mount( - - ); - expect(component).toMatchSnapshot(); - }); - }); - describe('Advanced setting set to grouped', () => { - it('renders individual items if there are less than 7', () => { - const links = getMockChromeNavLink().slice(0, 5); - const component = mount(); - expect(component).toMatchSnapshot(); - }); - it('renders individual items if there is only 1 category', () => { - // management doesn't count as a category - const navLinks = [ - makeLink('discover', 100, analyze), - makeLink('siem', 500, analyze), - makeLink('metrics', 600, analyze), - makeLink('monitoring', 800, analyze), - makeLink('visualize', 200, analyze), - makeLink('dashboard', 300, management), - makeLink('canvas', 400, management), - makeLink('logs', 700, management), - ]; - const component = mount(); - expect(component).toMatchSnapshot(); - }); - it('renders grouped items', () => { - const component = mount(); - expect(component).toMatchSnapshot(); - }); - }); -}); diff --git a/src/core/public/chrome/ui/header/nav_drawer.tsx b/src/core/public/chrome/ui/header/nav_drawer.tsx index dbb68d5dd3901e..c57faec1e428d7 100644 --- a/src/core/public/chrome/ui/header/nav_drawer.tsx +++ b/src/core/public/chrome/ui/header/nav_drawer.tsx @@ -18,39 +18,16 @@ */ import React from 'react'; -import { groupBy, sortBy } from 'lodash'; import { i18n } from '@kbn/i18n'; // @ts-ignore import { EuiNavDrawer, EuiHorizontalRule, EuiNavDrawerGroup } from '@elastic/eui'; -import { NavSetting, OnIsLockedUpdate } from './'; +import { OnIsLockedUpdate } from './'; import { ChromeNavLink, ChromeRecentlyAccessedHistoryItem } from '../../..'; -import { AppCategory } from '../../../../types'; import { HttpStart } from '../../../http'; import { NavLink } from './nav_link'; import { RecentLinks } from './recent_links'; -function getAllCategories(allCategorizedLinks: Record) { - const allCategories = {} as Record; - - for (const [key, value] of Object.entries(allCategorizedLinks)) { - allCategories[key] = value[0].category; - } - - return allCategories; -} - -function getOrderedCategories( - mainCategories: Record, - categoryDictionary: ReturnType -) { - return sortBy( - Object.keys(mainCategories), - categoryName => categoryDictionary[categoryName]?.order - ); -} - export interface Props { - navSetting: NavSetting; isLocked?: boolean; onIsLockedUpdate?: OnIsLockedUpdate; navLinks: NavLink[]; @@ -60,26 +37,9 @@ export interface Props { } function navDrawerRenderer( - { - navSetting, - isLocked, - onIsLockedUpdate, - navLinks, - chromeNavLinks, - recentlyAccessedItems, - basePath, - }: Props, + { isLocked, onIsLockedUpdate, navLinks, chromeNavLinks, recentlyAccessedItems, basePath }: Props, ref: React.Ref ) { - const disableGroupedNavSetting = navSetting === 'individual'; - const groupedNavLinks = groupBy(navLinks, link => link?.category?.label); - const { undefined: unknowns, ...allCategorizedLinks } = groupedNavLinks; - const { Management: management, ...mainCategories } = allCategorizedLinks; - const categoryDictionary = getAllCategories(allCategorizedLinks); - const orderedCategories = getOrderedCategories(mainCategories, categoryDictionary); - const showUngroupedNav = - disableGroupedNavSetting || navLinks.length < 7 || Object.keys(mainCategories).length === 1; - return ( - {showUngroupedNav ? ( - - ) : ( - <> - { - const category = categoryDictionary[categoryName]!; - const links = mainCategories[categoryName]; - - if (links.length === 1) { - return { - ...links[0], - label: category.label, - iconType: category.euiIconType || links[0].iconType, - }; - } - - return { - 'data-test-subj': 'navDrawerCategory', - iconType: category.euiIconType, - label: category.label, - flyoutMenu: { - title: category.label, - listItems: sortBy(links, 'order').map(link => { - link['data-test-subj'] = 'navDrawerFlyoutLink'; - return link; - }), - }, - }; - }), - ...sortBy(unknowns, 'order'), - ]} - /> - - { - link['data-test-subj'] = 'navDrawerFlyoutLink'; - return link; - }), - }, - }, - ]} - /> - - )} + ); } diff --git a/src/legacy/core_plugins/kibana/index.js b/src/legacy/core_plugins/kibana/index.js index 8e6bae0b588bc0..221133a17d59ab 100644 --- a/src/legacy/core_plugins/kibana/index.js +++ b/src/legacy/core_plugins/kibana/index.js @@ -115,7 +115,7 @@ export default function(kibana) { { id: 'kibana:stack_management', title: i18n.translate('kbn.managementTitle', { - defaultMessage: 'Stack Management', + defaultMessage: 'Management', }), order: 9003, url: `${kbnBaseUrl}#/management`, diff --git a/src/legacy/core_plugins/kibana/public/home/np_ready/components/home.test.js b/src/legacy/core_plugins/kibana/public/home/np_ready/components/home.test.js index b0d94711be7b62..db24cb3e3c1b7e 100644 --- a/src/legacy/core_plugins/kibana/public/home/np_ready/components/home.test.js +++ b/src/legacy/core_plugins/kibana/public/home/np_ready/components/home.test.js @@ -131,7 +131,7 @@ describe('home', () => { test('should not render directory entry when showOnHomePage is false', async () => { const directoryEntry = { id: 'stack-management', - title: 'Stack Management', + title: 'Management', description: 'Your center console for managing the Elastic Stack.', icon: 'managementApp', path: 'management_landing_page', diff --git a/src/legacy/core_plugins/kibana/public/management/index.js b/src/legacy/core_plugins/kibana/public/management/index.js index 2cba9fab7be222..6a36391c56b5c3 100644 --- a/src/legacy/core_plugins/kibana/public/management/index.js +++ b/src/legacy/core_plugins/kibana/public/management/index.js @@ -69,7 +69,7 @@ export function updateLandingPage(version) {

diff --git a/src/legacy/core_plugins/kibana/ui_setting_defaults.js b/src/legacy/core_plugins/kibana/ui_setting_defaults.js index 744ede891b84a3..f92694eabe58d1 100644 --- a/src/legacy/core_plugins/kibana/ui_setting_defaults.js +++ b/src/legacy/core_plugins/kibana/ui_setting_defaults.js @@ -1174,24 +1174,5 @@ export function getUiSettingDefaults() { category: ['accessibility'], requiresPageReload: true, }, - pageNavigation: { - name: i18n.translate('kbn.advancedSettings.pageNavigationName', { - defaultMessage: 'Side nav style', - }), - value: 'grouped', - description: i18n.translate('kbn.advancedSettings.pageNavigationDesc', { - defaultMessage: 'Change the style of navigation', - }), - type: 'select', - options: ['grouped', 'individual'], - optionLabels: { - grouped: i18n.translate('kbn.advancedSettings.pageNavigationGrouped', { - defaultMessage: 'Grouped', - }), - individual: i18n.translate('kbn.advancedSettings.pageNavigationIndividual', { - defaultMessage: 'Individual', - }), - }, - }, }; } diff --git a/src/legacy/ui/public/management/breadcrumbs.ts b/src/legacy/ui/public/management/breadcrumbs.ts index 936e99caff565f..e6156b6639ac47 100644 --- a/src/legacy/ui/public/management/breadcrumbs.ts +++ b/src/legacy/ui/public/management/breadcrumbs.ts @@ -21,7 +21,7 @@ import { i18n } from '@kbn/i18n'; export const MANAGEMENT_BREADCRUMB = Object.freeze({ text: i18n.translate('common.ui.stackManagement.breadcrumb', { - defaultMessage: 'Stack Management', + defaultMessage: 'Management', }), href: '#/management', }); diff --git a/src/plugins/management/public/components/management_sidebar_nav/management_sidebar_nav.tsx b/src/plugins/management/public/components/management_sidebar_nav/management_sidebar_nav.tsx index 69ba813d2347ed..01a98eb0ddb1f8 100644 --- a/src/plugins/management/public/components/management_sidebar_nav/management_sidebar_nav.tsx +++ b/src/plugins/management/public/components/management_sidebar_nav/management_sidebar_nav.tsx @@ -168,8 +168,7 @@ export class ManagementSidebarNav extends React.Component<

{i18n.translate('management.nav.label', { - // todo - defaultMessage: 'Stack Management', + defaultMessage: 'Management', })}

diff --git a/src/plugins/management/public/legacy/sections_register.js b/src/plugins/management/public/legacy/sections_register.js index ca35db56c340be..63d919377f89ea 100644 --- a/src/plugins/management/public/legacy/sections_register.js +++ b/src/plugins/management/public/legacy/sections_register.js @@ -27,8 +27,7 @@ export class LegacyManagementAdapter { 'management', { display: i18n.translate('management.displayName', { - // todo - defaultMessage: 'Stack Management', + defaultMessage: 'Management', }), }, capabilities @@ -36,7 +35,6 @@ export class LegacyManagementAdapter { this.main.register('data', { display: i18n.translate('management.connectDataDisplayName', { - // todo defaultMessage: 'Connect Data', }), order: 0, diff --git a/src/plugins/management/public/management_app.tsx b/src/plugins/management/public/management_app.tsx index 02b3ea306c23d8..705d98eaaf2ffd 100644 --- a/src/plugins/management/public/management_app.tsx +++ b/src/plugins/management/public/management_app.tsx @@ -64,8 +64,7 @@ export class ManagementApp { coreStart.chrome.setBreadcrumbs([ { text: i18n.translate('management.breadcrumb', { - // todo - defaultMessage: 'Stack Management', + defaultMessage: 'Management', }), href: '#/management', }, diff --git a/src/plugins/management/public/plugin.ts b/src/plugins/management/public/plugin.ts index df2398412dac25..1c9e1d5c895509 100644 --- a/src/plugins/management/public/plugin.ts +++ b/src/plugins/management/public/plugin.ts @@ -36,8 +36,8 @@ export class ManagementPlugin implements Plugin { - before(async () => { - await PageObjects.settings.setNavType('individual'); - }); - beforeEach(async () => { await PageObjects.common.navigateToApp('app_status_start'); }); diff --git a/test/plugin_functional/test_suites/core_plugins/applications.ts b/test/plugin_functional/test_suites/core_plugins/applications.ts index 6567837f653095..f50d4605325560 100644 --- a/test/plugin_functional/test_suites/core_plugins/applications.ts +++ b/test/plugin_functional/test_suites/core_plugins/applications.ts @@ -122,7 +122,7 @@ export default function({ getService, getPageObjects }: PluginFunctionalProvider }); it('can navigate from NP apps to legacy apps', async () => { - await appsMenu.clickLink('Stack Management'); + await appsMenu.clickLink('Management'); await loadingScreenShown(); await testSubjects.existOrFail('managementNav'); }); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index b171863f26c21c..47bf2ae634048e 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -1449,7 +1449,6 @@ "kbn.management.indexPatterns.listBreadcrumb": "インデックスパターン", "kbn.management.indexPatternTable.createBtn": "インデックスパターンの作成", "kbn.management.indexPatternTable.title": "インデックスパターン", - "kbn.management.landing.header": "Kibana {version} 管理", "kbn.management.landing.subhead": "インデックス、インデックスパターン、保存されたオブジェクト、Kibana の設定、その他を管理します。", "kbn.management.landing.text": "すべてのツールの一覧は、左のメニューにあります。", "kbn.management.objects.confirmModalOptions.deleteButtonLabel": "削除", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 050e9bd40f58df..a94a602e48d9ba 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -1449,7 +1449,6 @@ "kbn.management.indexPatterns.listBreadcrumb": "索引模式", "kbn.management.indexPatternTable.createBtn": "创建索引模式", "kbn.management.indexPatternTable.title": "索引模式", - "kbn.management.landing.header": "Kibana {version} 管理", "kbn.management.landing.subhead": "管理您的索引、索引模式、已保存对象、Kibana 设置等等。", "kbn.management.landing.text": "在左侧菜单中可找到完整工具列表", "kbn.management.objects.confirmModalOptions.deleteButtonLabel": "删除", diff --git a/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts b/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts index 261a8533876195..0d4c6b2c876666 100644 --- a/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts +++ b/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts @@ -55,7 +55,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { expectSpaceSelector: false, } ); - await kibanaServer.uiSettings.replace({ pageNavigation: 'individual' }); + await kibanaServer.uiSettings.replace({}); await PageObjects.settings.navigateTo(); }); @@ -69,7 +69,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows management navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Stack Management']); + expect(navLinks).to.eql(['Management']); }); it(`allows settings to be changed`, async () => { @@ -125,7 +125,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows Management navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Stack Management']); + expect(navLinks).to.eql(['Management']); }); it(`does not allow settings to be changed`, async () => { @@ -176,7 +176,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows Management navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Discover', 'Stack Management']); + expect(navLinks).to.eql(['Discover', 'Management']); }); it(`does not allow navigation to advanced settings; redirects to management home`, async () => { diff --git a/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_spaces.ts b/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_spaces.ts index 53202089e8961c..fc4f385df36942 100644 --- a/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_spaces.ts +++ b/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_spaces.ts @@ -41,9 +41,8 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); - await PageObjects.settings.setNavType('individual'); const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.contain('Stack Management'); + expect(navLinks).to.contain('Management'); }); it(`allows settings to be changed`, async () => { diff --git a/x-pack/test/functional/apps/apm/feature_controls/apm_security.ts b/x-pack/test/functional/apps/apm/feature_controls/apm_security.ts index 7c9c9f9c8c155a..e2d5efac4644cc 100644 --- a/x-pack/test/functional/apps/apm/feature_controls/apm_security.ts +++ b/x-pack/test/functional/apps/apm/feature_controls/apm_security.ts @@ -60,7 +60,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows apm navlink', async () => { const navLinks = await appsMenu.readLinks(); - expect(navLinks.map(link => link.text)).to.eql(['APM', 'Stack Management']); + expect(navLinks.map(link => link.text)).to.eql(['APM', 'Management']); }); it('can navigate to APM app', async () => { @@ -109,7 +109,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows apm navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['APM', 'Stack Management']); + expect(navLinks).to.eql(['APM', 'Management']); }); it('can navigate to APM app', async () => { diff --git a/x-pack/test/functional/apps/apm/feature_controls/apm_spaces.ts b/x-pack/test/functional/apps/apm/feature_controls/apm_spaces.ts index 474240b201face..1ac1784e0e05db 100644 --- a/x-pack/test/functional/apps/apm/feature_controls/apm_spaces.ts +++ b/x-pack/test/functional/apps/apm/feature_controls/apm_spaces.ts @@ -8,7 +8,7 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function({ getPageObjects, getService }: FtrProviderContext) { const spacesService = getService('spaces'); - const PageObjects = getPageObjects(['common', 'error', 'timePicker', 'security', 'settings']); + const PageObjects = getPageObjects(['common', 'error', 'timePicker', 'security']); const testSubjects = getService('testSubjects'); const appsMenu = getService('appsMenu'); @@ -30,7 +30,6 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); - await PageObjects.settings.setNavType('individual'); const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.contain('APM'); }); diff --git a/x-pack/test/functional/apps/canvas/feature_controls/canvas_security.ts b/x-pack/test/functional/apps/canvas/feature_controls/canvas_security.ts index 71c10bd8248be8..d0e37ec8e3f359 100644 --- a/x-pack/test/functional/apps/canvas/feature_controls/canvas_security.ts +++ b/x-pack/test/functional/apps/canvas/feature_controls/canvas_security.ts @@ -66,7 +66,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows canvas navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Canvas', 'Stack Management']); + expect(navLinks).to.eql(['Canvas', 'Management']); }); it(`landing page shows "Create new workpad" button`, async () => { @@ -142,7 +142,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows canvas navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Canvas', 'Stack Management']); + expect(navLinks).to.eql(['Canvas', 'Management']); }); it(`landing page shows disabled "Create new workpad" button`, async () => { diff --git a/x-pack/test/functional/apps/canvas/feature_controls/canvas_spaces.ts b/x-pack/test/functional/apps/canvas/feature_controls/canvas_spaces.ts index 5395f125bbd22b..28b572401892b5 100644 --- a/x-pack/test/functional/apps/canvas/feature_controls/canvas_spaces.ts +++ b/x-pack/test/functional/apps/canvas/feature_controls/canvas_spaces.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function({ getPageObjects, getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const spacesService = getService('spaces'); - const PageObjects = getPageObjects(['common', 'canvas', 'security', 'spaceSelector', 'settings']); + const PageObjects = getPageObjects(['common', 'canvas', 'security', 'spaceSelector']); const appsMenu = getService('appsMenu'); describe('spaces feature controls', function() { @@ -40,7 +40,6 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); - await PageObjects.settings.setNavType('individual'); const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.contain('Canvas'); }); diff --git a/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_security.ts b/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_security.ts index bfffefaecd94ce..b966d37becc3f0 100644 --- a/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_security.ts +++ b/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_security.ts @@ -77,7 +77,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows dashboard navlink', async () => { const navLinks = await appsMenu.readLinks(); - expect(navLinks.map(link => link.text)).to.eql(['Dashboard', 'Stack Management']); + expect(navLinks.map(link => link.text)).to.eql(['Dashboard', 'Management']); }); it(`landing page shows "Create new Dashboard" button`, async () => { @@ -261,7 +261,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows dashboard navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Dashboard', 'Stack Management']); + expect(navLinks).to.eql(['Dashboard', 'Management']); }); it(`landing page doesn't show "Create new Dashboard" button`, async () => { diff --git a/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_spaces.ts b/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_spaces.ts index 1f4f0f33a061e6..5ab26e41890965 100644 --- a/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_spaces.ts +++ b/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_spaces.ts @@ -14,13 +14,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const config = getService('config'); const spacesService = getService('spaces'); - const PageObjects = getPageObjects([ - 'common', - 'dashboard', - 'security', - 'spaceSelector', - 'settings', - ]); + const PageObjects = getPageObjects(['common', 'dashboard', 'security', 'spaceSelector']); const appsMenu = getService('appsMenu'); const testSubjects = getService('testSubjects'); @@ -50,7 +44,6 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); - await PageObjects.settings.setNavType('individual'); const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.contain('Dashboard'); }); 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 b521c47585d58b..b9c0b0095b96b7 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 @@ -37,10 +37,7 @@ export default function({ getService, getPageObjects }) { log.debug('Dashboard View Mode:initTests'); await esArchiver.loadIfNeeded('logstash_functional'); await esArchiver.load('dashboard_view_mode'); - await kibanaServer.uiSettings.replace({ - defaultIndex: 'logstash-*', - pageNavigation: 'individual', - }); + await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash-*' }); await browser.setWindowSize(1600, 1000); await PageObjects.common.navigateToApp('discover'); @@ -200,7 +197,7 @@ export default function({ getService, getPageObjects }) { await PageObjects.security.forceLogout(); await PageObjects.security.login('mixeduser', '123456'); - if (await appsMenu.linkExists('Stack Management')) { + if (await appsMenu.linkExists('Management')) { throw new Error('Expected management nav link to not be shown'); } }); @@ -209,7 +206,7 @@ export default function({ getService, getPageObjects }) { await PageObjects.security.forceLogout(); await PageObjects.security.login('mysuperuser', '123456'); - if (!(await appsMenu.linkExists('Stack Management'))) { + if (!(await appsMenu.linkExists('Management'))) { throw new Error('Expected management nav link to be shown'); } }); diff --git a/x-pack/test/functional/apps/dev_tools/feature_controls/dev_tools_security.ts b/x-pack/test/functional/apps/dev_tools/feature_controls/dev_tools_security.ts index 162bf23c294906..3d17d235b7f4fe 100644 --- a/x-pack/test/functional/apps/dev_tools/feature_controls/dev_tools_security.ts +++ b/x-pack/test/functional/apps/dev_tools/feature_controls/dev_tools_security.ts @@ -64,7 +64,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows Dev Tools navlink', async () => { const navLinks = await appsMenu.readLinks(); - expect(navLinks.map(link => link.text)).to.eql(['Dev Tools', 'Stack Management']); + expect(navLinks.map(link => link.text)).to.eql(['Dev Tools', 'Management']); }); describe('console', () => { @@ -145,7 +145,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it(`shows 'Dev Tools' navlink`, async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Dev Tools', 'Stack Management']); + expect(navLinks).to.eql(['Dev Tools', 'Management']); }); describe('console', () => { diff --git a/x-pack/test/functional/apps/dev_tools/feature_controls/dev_tools_spaces.ts b/x-pack/test/functional/apps/dev_tools/feature_controls/dev_tools_spaces.ts index 561b7f64eb77d6..d1eddfe89c59ed 100644 --- a/x-pack/test/functional/apps/dev_tools/feature_controls/dev_tools_spaces.ts +++ b/x-pack/test/functional/apps/dev_tools/feature_controls/dev_tools_spaces.ts @@ -10,13 +10,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const config = getService('config'); const spacesService = getService('spaces'); - const PageObjects = getPageObjects([ - 'common', - 'dashboard', - 'security', - 'spaceSelector', - 'settings', - ]); + const PageObjects = getPageObjects(['common', 'dashboard', 'security', 'spaceSelector']); const appsMenu = getService('appsMenu'); const testSubjects = getService('testSubjects'); const grokDebugger = getService('grokDebugger'); @@ -47,7 +41,6 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); - await PageObjects.settings.setNavType('individual'); const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.contain('Dev Tools'); }); diff --git a/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts b/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts index 53d0872b810fe1..87ae5231d10312 100644 --- a/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts +++ b/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts @@ -83,7 +83,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows discover navlink', async () => { const navLinks = await appsMenu.readLinks(); - expect(navLinks.map(link => link.text)).to.eql(['Discover', 'Stack Management']); + expect(navLinks.map(link => link.text)).to.eql(['Discover', 'Management']); }); it('shows save button', async () => { @@ -170,7 +170,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows discover navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Discover', 'Stack Management']); + expect(navLinks).to.eql(['Discover', 'Management']); }); it(`doesn't show save button`, async () => { diff --git a/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts b/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts index ba7d4077b27409..4bedc757f0b575 100644 --- a/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts +++ b/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts @@ -16,7 +16,6 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { 'timePicker', 'security', 'spaceSelector', - 'settings', ]); const testSubjects = getService('testSubjects'); const appsMenu = getService('appsMenu'); @@ -51,7 +50,6 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); - await PageObjects.settings.setNavType('individual'); const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.contain('Discover'); }); diff --git a/x-pack/test/functional/apps/graph/feature_controls/graph_security.ts b/x-pack/test/functional/apps/graph/feature_controls/graph_security.ts index 37de93a0a7e910..a2b062e6ef84fb 100644 --- a/x-pack/test/functional/apps/graph/feature_controls/graph_security.ts +++ b/x-pack/test/functional/apps/graph/feature_controls/graph_security.ts @@ -64,7 +64,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows graph navlink', async () => { const navLinks = await appsMenu.readLinks(); - expect(navLinks.map(link => link.text)).to.eql(['Graph', 'Stack Management']); + expect(navLinks.map(link => link.text)).to.eql(['Graph', 'Management']); }); it('landing page shows "Create new graph" button', async () => { @@ -127,7 +127,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows graph navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Graph', 'Stack Management']); + expect(navLinks).to.eql(['Graph', 'Management']); }); it('does not show a "Create new Workspace" button', async () => { diff --git a/x-pack/test/functional/apps/graph/feature_controls/graph_spaces.ts b/x-pack/test/functional/apps/graph/feature_controls/graph_spaces.ts index d0d0232b5a8b14..a0b0d5bef96680 100644 --- a/x-pack/test/functional/apps/graph/feature_controls/graph_spaces.ts +++ b/x-pack/test/functional/apps/graph/feature_controls/graph_spaces.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function({ getPageObjects, getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const spacesService = getService('spaces'); - const PageObjects = getPageObjects(['common', 'graph', 'security', 'error', 'settings']); + const PageObjects = getPageObjects(['common', 'graph', 'security', 'error']); const testSubjects = getService('testSubjects'); const appsMenu = getService('appsMenu'); @@ -34,7 +34,6 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); - await PageObjects.settings.setNavType('individual'); const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.contain('Graph'); }); diff --git a/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_security.ts b/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_security.ts index 0783767d8f1521..d72c9b970204a4 100644 --- a/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_security.ts +++ b/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_security.ts @@ -71,7 +71,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows management navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Stack Management']); + expect(navLinks).to.eql(['Management']); }); it(`index pattern listing shows create button`, async () => { @@ -114,7 +114,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { } ); - await kibanaServer.uiSettings.replace({ pageNavigation: 'individual' }); + await kibanaServer.uiSettings.replace({}); await PageObjects.settings.navigateTo(); }); @@ -125,7 +125,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows management navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Stack Management']); + expect(navLinks).to.eql(['Management']); }); it(`index pattern listing doesn't show create button`, async () => { @@ -177,7 +177,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows Management navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Discover', 'Stack Management']); + expect(navLinks).to.eql(['Discover', 'Management']); }); it(`doesn't show Index Patterns in management side-nav`, async () => { diff --git a/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_spaces.ts b/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_spaces.ts index d4422e94d10cf5..7d9bee37bbbc4e 100644 --- a/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_spaces.ts +++ b/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_spaces.ts @@ -41,9 +41,8 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); - await PageObjects.settings.setNavType('individual'); const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.contain('Stack Management'); + expect(navLinks).to.contain('Management'); }); it(`index pattern listing shows create button`, async () => { diff --git a/x-pack/test/functional/apps/infra/feature_controls/infrastructure_security.ts b/x-pack/test/functional/apps/infra/feature_controls/infrastructure_security.ts index ede77b7d9afa7f..bf35d4dc06aa2b 100644 --- a/x-pack/test/functional/apps/infra/feature_controls/infrastructure_security.ts +++ b/x-pack/test/functional/apps/infra/feature_controls/infrastructure_security.ts @@ -61,7 +61,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows metrics navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Metrics', 'Stack Management']); + expect(navLinks).to.eql(['Metrics', 'Management']); }); describe('infrastructure landing page without data', () => { @@ -177,7 +177,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows metrics navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Metrics', 'Stack Management']); + expect(navLinks).to.eql(['Metrics', 'Management']); }); describe('infrastructure landing page without data', () => { diff --git a/x-pack/test/functional/apps/infra/feature_controls/infrastructure_spaces.ts b/x-pack/test/functional/apps/infra/feature_controls/infrastructure_spaces.ts index 3bbcc1aa7043c0..37056c7f17ca11 100644 --- a/x-pack/test/functional/apps/infra/feature_controls/infrastructure_spaces.ts +++ b/x-pack/test/functional/apps/infra/feature_controls/infrastructure_spaces.ts @@ -9,13 +9,7 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function({ getPageObjects, getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const spacesService = getService('spaces'); - const PageObjects = getPageObjects([ - 'common', - 'infraHome', - 'security', - 'spaceSelector', - 'settings', - ]); + const PageObjects = getPageObjects(['common', 'infraHome', 'security', 'spaceSelector']); const testSubjects = getService('testSubjects'); const appsMenu = getService('appsMenu'); @@ -47,7 +41,6 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); - await PageObjects.settings.setNavType('individual'); const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.contain('Metrics'); }); diff --git a/x-pack/test/functional/apps/infra/feature_controls/logs_security.ts b/x-pack/test/functional/apps/infra/feature_controls/logs_security.ts index 48ad4e90fd413f..e5a6e27a0fadbe 100644 --- a/x-pack/test/functional/apps/infra/feature_controls/logs_security.ts +++ b/x-pack/test/functional/apps/infra/feature_controls/logs_security.ts @@ -58,7 +58,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows logs navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Logs', 'Stack Management']); + expect(navLinks).to.eql(['Logs', 'Management']); }); describe('logs landing page without data', () => { @@ -121,7 +121,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows logs navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Logs', 'Stack Management']); + expect(navLinks).to.eql(['Logs', 'Management']); }); describe('logs landing page without data', () => { diff --git a/x-pack/test/functional/apps/infra/feature_controls/logs_spaces.ts b/x-pack/test/functional/apps/infra/feature_controls/logs_spaces.ts index 0094d227514c00..985131113c5350 100644 --- a/x-pack/test/functional/apps/infra/feature_controls/logs_spaces.ts +++ b/x-pack/test/functional/apps/infra/feature_controls/logs_spaces.ts @@ -9,13 +9,7 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function({ getPageObjects, getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const spacesService = getService('spaces'); - const PageObjects = getPageObjects([ - 'common', - 'infraHome', - 'security', - 'spaceSelector', - 'settings', - ]); + const PageObjects = getPageObjects(['common', 'infraHome', 'security', 'spaceSelector']); const testSubjects = getService('testSubjects'); const appsMenu = getService('appsMenu'); @@ -42,7 +36,6 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); - await PageObjects.settings.setNavType('individual'); const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.contain('Logs'); }); diff --git a/x-pack/test/functional/apps/machine_learning/feature_controls/ml_security.ts b/x-pack/test/functional/apps/machine_learning/feature_controls/ml_security.ts index c25c1bfe4b7318..8fb6f21c778d3f 100644 --- a/x-pack/test/functional/apps/machine_learning/feature_controls/ml_security.ts +++ b/x-pack/test/functional/apps/machine_learning/feature_controls/ml_security.ts @@ -10,7 +10,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const security = getService('security'); const appsMenu = getService('appsMenu'); - const PageObjects = getPageObjects(['common', 'security', 'settings']); + const PageObjects = getPageObjects(['common', 'security']); describe('security', () => { before(async () => { @@ -94,7 +94,6 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { }); await PageObjects.security.login('machine_learning_user', 'machine_learning_user-password'); - await PageObjects.settings.setNavType('individual'); }); after(async () => { diff --git a/x-pack/test/functional/apps/machine_learning/feature_controls/ml_spaces.ts b/x-pack/test/functional/apps/machine_learning/feature_controls/ml_spaces.ts index c633852a2da0a7..fc94688e98811b 100644 --- a/x-pack/test/functional/apps/machine_learning/feature_controls/ml_spaces.ts +++ b/x-pack/test/functional/apps/machine_learning/feature_controls/ml_spaces.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function({ getPageObjects, getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const spacesService = getService('spaces'); - const PageObjects = getPageObjects(['common', 'dashboard', 'security', 'error', 'settings']); + const PageObjects = getPageObjects(['common', 'dashboard', 'security', 'error']); const appsMenu = getService('appsMenu'); const testSubjects = getService('testSubjects'); @@ -39,7 +39,6 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); - await PageObjects.settings.setNavType('individual'); const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.contain('Machine Learning'); }); diff --git a/x-pack/test/functional/apps/maps/feature_controls/maps_security.ts b/x-pack/test/functional/apps/maps/feature_controls/maps_security.ts index ece162cbd96cc7..804ad5725edfd3 100644 --- a/x-pack/test/functional/apps/maps/feature_controls/maps_security.ts +++ b/x-pack/test/functional/apps/maps/feature_controls/maps_security.ts @@ -66,7 +66,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows maps navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Maps', 'Stack Management']); + expect(navLinks).to.eql(['Maps', 'Management']); }); it(`allows a map to be created`, async () => { @@ -153,7 +153,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows Maps navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Maps', 'Stack Management']); + expect(navLinks).to.eql(['Maps', 'Management']); }); it(`does not show create new button`, async () => { @@ -248,7 +248,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('does not show Maps navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Discover', 'Stack Management']); + expect(navLinks).to.eql(['Discover', 'Management']); }); it(`returns a 404`, async () => { diff --git a/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_security.ts b/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_security.ts index 130aefb3cae2ac..d985da42ab5eda 100644 --- a/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_security.ts +++ b/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_security.ts @@ -10,7 +10,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const security = getService('security'); const appsMenu = getService('appsMenu'); - const PageObjects = getPageObjects(['common', 'security', 'settings']); + const PageObjects = getPageObjects(['common', 'security']); describe('security', () => { before(async () => { @@ -97,7 +97,6 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { }); it('shows monitoring navlink', async () => { - await PageObjects.settings.setNavType('individual'); const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.contain('Stack Monitoring'); }); diff --git a/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_spaces.ts b/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_spaces.ts index 0465cbcf54541c..9e306b074d2140 100644 --- a/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_spaces.ts +++ b/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_spaces.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function({ getPageObjects, getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const spacesService = getService('spaces'); - const PageObjects = getPageObjects(['common', 'dashboard', 'security', 'error', 'settings']); + const PageObjects = getPageObjects(['common', 'dashboard', 'security', 'error']); const appsMenu = getService('appsMenu'); const find = getService('find'); @@ -41,7 +41,6 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); - await PageObjects.settings.setNavType('individual'); const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.contain('Stack Monitoring'); }); diff --git a/x-pack/test/functional/apps/spaces/feature_controls/spaces_security.ts b/x-pack/test/functional/apps/spaces/feature_controls/spaces_security.ts index 9ca314ba5ec183..49b684a37079e8 100644 --- a/x-pack/test/functional/apps/spaces/feature_controls/spaces_security.ts +++ b/x-pack/test/functional/apps/spaces/feature_controls/spaces_security.ts @@ -16,7 +16,6 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { describe('security feature controls', () => { before(async () => { await esArchiver.load('empty_kibana'); - await PageObjects.settings.setNavType('individual'); }); after(async () => { @@ -57,7 +56,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows management navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.contain('Stack Management'); + expect(navLinks).to.contain('Management'); }); it(`displays Spaces management section`, async () => { @@ -135,7 +134,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows management navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.contain('Stack Management'); + expect(navLinks).to.contain('Management'); }); it(`doesn't display Spaces management section`, async () => { diff --git a/x-pack/test/functional/apps/timelion/feature_controls/timelion_security.ts b/x-pack/test/functional/apps/timelion/feature_controls/timelion_security.ts index 62483a10552e34..dea45f161e4510 100644 --- a/x-pack/test/functional/apps/timelion/feature_controls/timelion_security.ts +++ b/x-pack/test/functional/apps/timelion/feature_controls/timelion_security.ts @@ -60,7 +60,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows timelion navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Timelion', 'Stack Management']); + expect(navLinks).to.eql(['Timelion', 'Management']); }); it(`allows a timelion sheet to be created`, async () => { @@ -112,7 +112,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows timelion navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Timelion', 'Stack Management']); + expect(navLinks).to.eql(['Timelion', 'Management']); }); it(`does not allow a timelion sheet to be created`, async () => { diff --git a/x-pack/test/functional/apps/timelion/feature_controls/timelion_spaces.ts b/x-pack/test/functional/apps/timelion/feature_controls/timelion_spaces.ts index 7e0fe731301a64..fb203a23359bdd 100644 --- a/x-pack/test/functional/apps/timelion/feature_controls/timelion_spaces.ts +++ b/x-pack/test/functional/apps/timelion/feature_controls/timelion_spaces.ts @@ -9,13 +9,7 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function({ getPageObjects, getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const spacesService = getService('spaces'); - const PageObjects = getPageObjects([ - 'common', - 'timelion', - 'security', - 'spaceSelector', - 'settings', - ]); + const PageObjects = getPageObjects(['common', 'timelion', 'security', 'spaceSelector']); const appsMenu = getService('appsMenu'); describe('timelion', () => { @@ -44,7 +38,6 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); - await PageObjects.settings.setNavType('individual'); const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.contain('Timelion'); }); diff --git a/x-pack/test/functional/apps/uptime/feature_controls/uptime_security.ts b/x-pack/test/functional/apps/uptime/feature_controls/uptime_security.ts index 4ff82484db91c4..a004f8db66823c 100644 --- a/x-pack/test/functional/apps/uptime/feature_controls/uptime_security.ts +++ b/x-pack/test/functional/apps/uptime/feature_controls/uptime_security.ts @@ -64,7 +64,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows uptime navlink', async () => { const navLinks = await appsMenu.readLinks(); - expect(navLinks.map(link => link.text)).to.eql(['Uptime', 'Stack Management']); + expect(navLinks.map(link => link.text)).to.eql(['Uptime', 'Management']); }); it('can navigate to Uptime app', async () => { @@ -115,7 +115,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows uptime navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Uptime', 'Stack Management']); + expect(navLinks).to.eql(['Uptime', 'Management']); }); it('can navigate to Uptime app', async () => { diff --git a/x-pack/test/functional/apps/uptime/feature_controls/uptime_spaces.ts b/x-pack/test/functional/apps/uptime/feature_controls/uptime_spaces.ts index c3dcb1b27771fb..77c5b323340bf8 100644 --- a/x-pack/test/functional/apps/uptime/feature_controls/uptime_spaces.ts +++ b/x-pack/test/functional/apps/uptime/feature_controls/uptime_spaces.ts @@ -8,7 +8,7 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function({ getPageObjects, getService }: FtrProviderContext) { const spacesService = getService('spaces'); - const PageObjects = getPageObjects(['common', 'error', 'timePicker', 'security', 'settings']); + const PageObjects = getPageObjects(['common', 'error', 'timePicker', 'security']); const testSubjects = getService('testSubjects'); const appsMenu = getService('appsMenu'); @@ -30,7 +30,6 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); - await PageObjects.settings.setNavType('individual'); const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.contain('Uptime'); }); diff --git a/x-pack/test/functional/apps/visualize/feature_controls/visualize_security.ts b/x-pack/test/functional/apps/visualize/feature_controls/visualize_security.ts index 1876f460383263..e5b6512d1c1b07 100644 --- a/x-pack/test/functional/apps/visualize/feature_controls/visualize_security.ts +++ b/x-pack/test/functional/apps/visualize/feature_controls/visualize_security.ts @@ -76,7 +76,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows visualize navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Visualize', 'Stack Management']); + expect(navLinks).to.eql(['Visualize', 'Management']); }); it(`landing page shows "Create new Visualization" button`, async () => { @@ -200,7 +200,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('shows visualize navlink', async () => { const navLinks = (await appsMenu.readLinks()).map(link => link.text); - expect(navLinks).to.eql(['Visualize', 'Stack Management']); + expect(navLinks).to.eql(['Visualize', 'Management']); }); it(`landing page shows "Create new Visualization" button`, async () => { diff --git a/x-pack/test/functional/apps/visualize/feature_controls/visualize_spaces.ts b/x-pack/test/functional/apps/visualize/feature_controls/visualize_spaces.ts index b1cb156caad90d..4f12dd16247f62 100644 --- a/x-pack/test/functional/apps/visualize/feature_controls/visualize_spaces.ts +++ b/x-pack/test/functional/apps/visualize/feature_controls/visualize_spaces.ts @@ -11,13 +11,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const config = getService('config'); const spacesService = getService('spaces'); - const PageObjects = getPageObjects([ - 'common', - 'visualize', - 'security', - 'spaceSelector', - 'settings', - ]); + const PageObjects = getPageObjects(['common', 'visualize', 'security', 'spaceSelector']); const testSubjects = getService('testSubjects'); const appsMenu = getService('appsMenu'); @@ -47,7 +41,6 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToApp('home', { basePath: '/s/custom_space', }); - await PageObjects.settings.setNavType('individual'); const navLinks = (await appsMenu.readLinks()).map(link => link.text); expect(navLinks).to.contain('Visualize'); });