From 9b07ddc1e25458aacc2703900d16c9e769f06bcd Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Fri, 20 Mar 2020 14:45:00 +0100 Subject: [PATCH 1/9] [Discover] Remove StateManagementConfigProvider (#60221) * Remove StateManagementConfigProvider from Discover * Code cleanups --- .../__tests__/directives/field_chooser.js | 17 +---------- .../__tests__/doc_table/lib/rows_headers.js | 15 +--------- .../public/discover/get_inner_angular.ts | 30 ++----------------- .../discover/np_ready/angular/context.js | 6 ++-- .../discover/np_ready/angular/context_app.js | 8 ++--- .../np_ready/angular/directives/field_name.js | 6 ++-- .../discover/np_ready/angular/discover.js | 5 ++-- .../doc_table/components/table_header.ts | 7 +++-- .../angular/doc_table/components/table_row.ts | 6 ++-- .../np_ready/angular/doc_table/doc_table.ts | 10 ++----- .../components/field_chooser/field_chooser.js | 4 ++- 11 files changed, 29 insertions(+), 85 deletions(-) diff --git a/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/field_chooser.js b/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/field_chooser.js index f74e145865475..47392c541890e 100644 --- a/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/field_chooser.js +++ b/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/field_chooser.js @@ -73,22 +73,7 @@ describe('discover field chooser directives', function() { beforeEach(() => pluginInstance.initializeServices()); beforeEach(() => pluginInstance.initializeInnerAngular()); - beforeEach( - ngMock.module('app/discover', $provide => { - $provide.decorator('config', $delegate => { - // disable shortDots for these tests - $delegate.get = _.wrap($delegate.get, function(origGet, name) { - if (name === 'shortDots:enable') { - return false; - } else { - return origGet.call(this, name); - } - }); - - return $delegate; - }); - }) - ); + beforeEach(ngMock.module('app/discover')); beforeEach( ngMock.inject(function(Private) { diff --git a/src/legacy/core_plugins/kibana/public/discover/__tests__/doc_table/lib/rows_headers.js b/src/legacy/core_plugins/kibana/public/discover/__tests__/doc_table/lib/rows_headers.js index c19e033ccb72d..9b63b8cd18f3e 100644 --- a/src/legacy/core_plugins/kibana/public/discover/__tests__/doc_table/lib/rows_headers.js +++ b/src/legacy/core_plugins/kibana/public/discover/__tests__/doc_table/lib/rows_headers.js @@ -30,7 +30,6 @@ import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logsta describe('Doc Table', function() { let $parentScope; let $scope; - let config; // Stub out a minimal mapping of 4 fields let mapping; @@ -41,8 +40,7 @@ describe('Doc Table', function() { beforeEach(() => pluginInstance.initializeInnerAngular()); beforeEach(ngMock.module('app/discover')); beforeEach( - ngMock.inject(function(_config_, $rootScope, Private) { - config = _config_; + ngMock.inject(function($rootScope, Private) { $parentScope = $rootScope; $parentScope.indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); mapping = $parentScope.indexPattern.fields; @@ -144,12 +142,6 @@ describe('Doc Table', function() { filter: sinon.spy(), maxLength: 50, }); - - // Ignore the metaFields (_id, _type, etc) since we don't have a mapping for them - sinon - .stub(config, 'get') - .withArgs('metaFields') - .returns([]); }); afterEach(function() { destroy(); @@ -215,11 +207,6 @@ describe('Doc Table', function() { maxLength: 50, }); - sinon - .stub(config, 'get') - .withArgs('metaFields') - .returns(['_id']); - // Open the row $scope.toggleRow(); $scope.$digest(); diff --git a/src/legacy/core_plugins/kibana/public/discover/get_inner_angular.ts b/src/legacy/core_plugins/kibana/public/discover/get_inner_angular.ts index 4d871bcb7a858..a19278911507c 100644 --- a/src/legacy/core_plugins/kibana/public/discover/get_inner_angular.ts +++ b/src/legacy/core_plugins/kibana/public/discover/get_inner_angular.ts @@ -23,9 +23,7 @@ import angular from 'angular'; import { EuiIcon } from '@elastic/eui'; import { i18nDirective, i18nFilter, I18nProvider } from '@kbn/i18n/angular'; -import { CoreStart, LegacyCoreStart, IUiSettingsClient } from 'kibana/public'; -// @ts-ignore -import { StateManagementConfigProvider } from 'ui/state_management/config_provider'; +import { CoreStart, LegacyCoreStart } from 'kibana/public'; // @ts-ignore import { KbnUrlProvider } from 'ui/url'; import { DataPublicPluginStart } from '../../../../../plugins/data/public'; @@ -108,7 +106,6 @@ export function initializeInnerAngularModule( createLocalI18nModule(); createLocalPrivateModule(); createLocalPromiseModule(); - createLocalConfigModule(core.uiSettings); createLocalKbnUrlModule(); createLocalTopNavModule(navigation); createLocalStorageModule(); @@ -143,7 +140,6 @@ export function initializeInnerAngularModule( 'ngRoute', 'react', 'ui.bootstrap', - 'discoverConfig', 'discoverI18n', 'discoverPrivate', 'discoverPromise', @@ -176,21 +172,6 @@ function createLocalKbnUrlModule() { .service('kbnUrl', (Private: IPrivate) => Private(KbnUrlProvider)); } -function createLocalConfigModule(uiSettings: IUiSettingsClient) { - angular - .module('discoverConfig', ['discoverPrivate']) - .provider('stateManagementConfig', StateManagementConfigProvider) - .provider('config', () => { - return { - $get: () => ({ - get: (value: string) => { - return uiSettings ? uiSettings.get(value) : undefined; - }, - }), - }; - }); -} - function createLocalPromiseModule() { angular.module('discoverPromise', []).service('Promise', PromiseServiceCreator); } @@ -229,7 +210,7 @@ const createLocalStorageService = function(type: string) { function createElasticSearchModule(data: DataPublicPluginStart) { angular - .module('discoverEs', ['discoverConfig']) + .module('discoverEs', []) // Elasticsearch client used for requesting data. Connects to the /elasticsearch proxy .service('es', () => { return data.search.__LEGACY.esClient; @@ -242,12 +223,7 @@ function createPagerFactoryModule() { function createDocTableModule() { angular - .module('discoverDocTable', [ - 'discoverKbnUrl', - 'discoverConfig', - 'discoverPagerFactory', - 'react', - ]) + .module('discoverDocTable', ['discoverKbnUrl', 'discoverPagerFactory', 'react']) .directive('docTable', createDocTableDirective) .directive('kbnTableHeader', createTableHeaderDirective) .directive('toolBarPagerText', createToolBarPagerTextDirective) diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context.js index 038f783a0daf1..f8e764cbcbebb 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context.js @@ -66,7 +66,7 @@ getAngularModule().config($routeProvider => { }); }); -function ContextAppRouteController($routeParams, $scope, config, $route) { +function ContextAppRouteController($routeParams, $scope, $route) { const filterManager = getServices().filterManager; const indexPattern = $route.current.locals.indexPattern.ip; const { @@ -77,9 +77,9 @@ function ContextAppRouteController($routeParams, $scope, config, $route) { setFilters, setAppState, } = getState({ - defaultStepSize: config.get('context:defaultSize'), + defaultStepSize: getServices().uiSettings.get('context:defaultSize'), timeFieldName: indexPattern.timeFieldName, - storeInSessionStorage: config.get('state:storeInSessionStorage'), + storeInSessionStorage: getServices().uiSettings.get('state:storeInSessionStorage'), }); this.state = { ...appState.getState() }; this.anchorId = $routeParams.id; diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context_app.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context_app.js index 345717cafee9a..a6a1de695156d 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context_app.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context_app.js @@ -57,13 +57,13 @@ module.directive('contextApp', function ContextApp() { }; }); -function ContextAppController($scope, config, Private) { - const { filterManager, indexpatterns } = getServices(); +function ContextAppController($scope, Private) { + const { filterManager, indexpatterns, uiSettings } = getServices(); const queryParameterActions = getQueryParameterActions(filterManager, indexpatterns); const queryActions = Private(QueryActionsProvider); this.state = createInitialState( - parseInt(config.get('context:step'), 10), - getFirstSortableField(this.indexPattern, config.get('context:tieBreakerFields')), + parseInt(uiSettings.get('context:step'), 10), + getFirstSortableField(this.indexPattern, uiSettings.get('context:tieBreakerFields')), this.discoverUrl ); diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/field_name.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/field_name.js index 4bc498928be52..b020113381992 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/field_name.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/field_name.js @@ -17,9 +17,9 @@ * under the License. */ import { FieldName } from './field_name/field_name'; -import { wrapInI18nContext } from '../../../kibana_services'; +import { getServices, wrapInI18nContext } from '../../../kibana_services'; -export function FieldNameDirectiveProvider(config, reactDirective) { +export function FieldNameDirectiveProvider(reactDirective) { return reactDirective( wrapInI18nContext(FieldName), [ @@ -29,7 +29,7 @@ export function FieldNameDirectiveProvider(config, reactDirective) { ], { restrict: 'AE' }, { - useShortDots: config.get('shortDots:enable'), + useShortDots: getServices().uiSettings.get('shortDots:enable'), } ); } diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js index 9a383565f4f43..2857f8720d8dc 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js @@ -64,7 +64,7 @@ const { share, timefilter, toastNotifications, - uiSettings, + uiSettings: config, visualizations, } = getServices(); @@ -131,7 +131,7 @@ app.config($routeProvider => { * * @type {State} */ - const id = getIndexPatternId(index, indexPatternList, uiSettings.get('defaultIndex')); + const id = getIndexPatternId(index, indexPatternList, config.get('defaultIndex')); return Promise.props({ list: indexPatternList, loaded: indexPatterns.get(id), @@ -184,7 +184,6 @@ function discoverController( $timeout, $window, Promise, - config, kbnUrl, localStorage, uiCapabilities diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc_table/components/table_header.ts b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc_table/components/table_header.ts index 32174984c1dfb..84d865fd22a9a 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc_table/components/table_header.ts +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc_table/components/table_header.ts @@ -16,11 +16,12 @@ * specific language governing permissions and limitations * under the License. */ -import { IUiSettingsClient } from 'kibana/public'; import { TableHeader } from './table_header/table_header'; -import { wrapInI18nContext } from '../../../../kibana_services'; +import { wrapInI18nContext, getServices } from '../../../../kibana_services'; + +export function createTableHeaderDirective(reactDirective: any) { + const { uiSettings: config } = getServices(); -export function createTableHeaderDirective(reactDirective: any, config: IUiSettingsClient) { return reactDirective( wrapInI18nContext(TableHeader), [ diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc_table/components/table_row.ts b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc_table/components/table_row.ts index 7a090d6b7820c..5d3f6ac199a46 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc_table/components/table_row.ts +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc_table/components/table_row.ts @@ -19,7 +19,6 @@ import _ from 'lodash'; import $ from 'jquery'; -import { IUiSettingsClient } from 'kibana/public'; // @ts-ignore import rison from 'rison-node'; import '../../doc_viewer'; @@ -45,8 +44,7 @@ interface LazyScope extends ng.IScope { export function createTableRowDirective( $compile: ng.ICompileService, $httpParamSerializer: any, - kbnUrl: any, - config: IUiSettingsClient + kbnUrl: any ) { const cellTemplate = _.template(noWhiteSpace(cellTemplateHtml)); const truncateByHeightTemplate = _.template(noWhiteSpace(truncateByHeightTemplateHtml)); @@ -140,7 +138,7 @@ export function createTableRowDirective( const newHtmls = [openRowHtml]; const mapping = indexPattern.fields.getByName; - const hideTimeColumn = config.get('doc_table:hideTimeColumn'); + const hideTimeColumn = getServices().uiSettings.get('doc_table:hideTimeColumn'); if (indexPattern.timeFieldName && !hideTimeColumn) { newHtmls.push( cellTemplate({ diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc_table/doc_table.ts b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc_table/doc_table.ts index 0ca8286c17081..3cb3a460af649 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc_table/doc_table.ts +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc_table/doc_table.ts @@ -17,21 +17,17 @@ * under the License. */ -import { IUiSettingsClient } from 'kibana/public'; import html from './doc_table.html'; import { dispatchRenderComplete } from '../../../../../../../../plugins/kibana_utils/public'; // @ts-ignore import { getLimitedSearchResultsMessage } from './doc_table_strings'; +import { getServices } from '../../../kibana_services'; interface LazyScope extends ng.IScope { [key: string]: any; } -export function createDocTableDirective( - config: IUiSettingsClient, - pagerFactory: any, - $filter: any -) { +export function createDocTableDirective(pagerFactory: any, $filter: any) { return { restrict: 'E', template: html, @@ -68,7 +64,7 @@ export function createDocTableDirective( }; $scope.limitedResultsWarning = getLimitedSearchResultsMessage( - config.get('discover:sampleSize') + getServices().uiSettings.get('discover:sampleSize') ); $scope.addRows = function() { diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/field_chooser.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/field_chooser.js index 4afaafd9bb1cf..398728e51862f 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/field_chooser.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/field_chooser.js @@ -29,8 +29,9 @@ import { KBN_FIELD_TYPES, } from '../../../../../../../../plugins/data/public'; import { getMapsAppUrl, isFieldVisualizable, isMapsAppRegistered } from './lib/visualize_url_utils'; +import { getServices } from '../../../kibana_services'; -export function createFieldChooserDirective($location, config) { +export function createFieldChooserDirective($location) { return { restrict: 'E', scope: { @@ -49,6 +50,7 @@ export function createFieldChooserDirective($location, config) { $scope.showFilter = false; $scope.toggleShowFilter = () => ($scope.showFilter = !$scope.showFilter); $scope.indexPatternList = _.sortBy($scope.indexPatternList, o => o.get('title')); + const config = getServices().uiSettings; const filter = ($scope.filter = { props: ['type', 'aggregatable', 'searchable', 'missing', 'name'], From 592ded89c069aac21f7a8fd217a4b9fde9429845 Mon Sep 17 00:00:00 2001 From: Aaron Caldwell Date: Fri, 20 Mar 2020 08:17:05 -0600 Subject: [PATCH 2/9] [Maps] Update layer dependencies to NP (#59585) * Layers dir up through sources migrated. Kibana services updated * Create separate init method for plugin setup, leverage in embeddable factory * Add NP timefilter, http, IndexPatternSelect * Pull vis color utils into Maps * Add NP dark mode and toast handling. Some fixes * Init autocomplete and indexPattern via normal paths * Test fixes and clean up * Update index pattern and autocomplete refs. Make getters functions * Fix remaining broken jest tests * Update inspector start contract * Clean up plugin and legacy files. Fix type issues * Set inspector in plugin start method not external function * Keep both injected var functions (legacy and NP). Move inspector init back to separate init function * Add back ts-ignore on NP kibana services import --- .../maps/public/angular/get_initial_layers.js | 4 +- .../public/angular/get_initial_layers.test.js | 17 +++--- .../maps/public/angular/map_controller.js | 10 ++-- .../public/components/geo_field_with_index.ts | 2 +- .../filter_editor/filter_editor.js | 4 +- .../layer_panel/join_editor/resources/join.js | 4 +- .../join_editor/resources/join_expression.js | 12 ++-- .../embeddable/map_embeddable_factory.js | 10 ++-- .../plugins/maps/public/index_pattern_util.js | 4 +- .../plugins/maps/public/kibana_services.js | 37 ++++++++++++- .../public/layers/joins/inner_join.test.js | 1 - .../sources/ems_tms_source/ems_tms_source.js | 6 +- .../layers/sources/ems_unavailable_message.js | 4 +- .../create_source_editor.js | 9 ++- .../update_source_editor.js | 4 +- .../es_pew_pew_source/create_source_editor.js | 9 ++- .../es_pew_pew_source/update_source_editor.js | 4 +- .../es_search_source/create_source_editor.js | 20 ++++--- .../es_search_source/es_search_source.test.ts | 6 ++ .../es_search_source/load_index_settings.js | 12 ++-- .../es_search_source/update_source_editor.js | 6 +- .../maps/public/layers/sources/es_source.js | 14 ++--- .../layers/sources/es_term_source.test.js | 1 - .../maps/public/layers/styles/color_utils.js | 22 ++++++-- .../symbol/vector_style_icon_editor.js | 6 +- .../layers/styles/vector/vector_style.test.js | 11 +++- .../styles/vector/vector_style_defaults.js | 4 +- x-pack/legacy/plugins/maps/public/legacy.ts | 6 +- x-pack/legacy/plugins/maps/public/plugin.ts | 55 ++++++++++++++++--- .../maps/public/selectors/map_selectors.js | 6 +- .../public/selectors/map_selectors.test.js | 6 +- 31 files changed, 207 insertions(+), 109 deletions(-) diff --git a/x-pack/legacy/plugins/maps/public/angular/get_initial_layers.js b/x-pack/legacy/plugins/maps/public/angular/get_initial_layers.js index 3cae75231d28e..8fc32aef54770 100644 --- a/x-pack/legacy/plugins/maps/public/angular/get_initial_layers.js +++ b/x-pack/legacy/plugins/maps/public/angular/get_initial_layers.js @@ -6,7 +6,7 @@ import _ from 'lodash'; import { KibanaTilemapSource } from '../layers/sources/kibana_tilemap_source'; import { EMSTMSSource } from '../layers/sources/ems_tms_source'; -import chrome from 'ui/chrome'; +import { getInjectedVarFunc } from '../kibana_services'; import { getKibanaTileMap } from '../meta'; export function getInitialLayers(layerListJSON, initialLayers = []) { @@ -22,7 +22,7 @@ export function getInitialLayers(layerListJSON, initialLayers = []) { return [layer.toLayerDescriptor(), ...initialLayers]; } - const isEmsEnabled = chrome.getInjected('isEmsEnabled', true); + const isEmsEnabled = getInjectedVarFunc()('isEmsEnabled', true); if (isEmsEnabled) { const descriptor = EMSTMSSource.createDescriptor({ isAutoSelect: true }); const source = new EMSTMSSource(descriptor); diff --git a/x-pack/legacy/plugins/maps/public/angular/get_initial_layers.test.js b/x-pack/legacy/plugins/maps/public/angular/get_initial_layers.test.js index a62d46475a549..f41ed26b2a05d 100644 --- a/x-pack/legacy/plugins/maps/public/angular/get_initial_layers.test.js +++ b/x-pack/legacy/plugins/maps/public/angular/get_initial_layers.test.js @@ -7,16 +7,17 @@ jest.mock('../meta', () => { return {}; }); - -jest.mock('ui/chrome', () => { - return {}; -}); +jest.mock('../kibana_services'); import { getInitialLayers } from './get_initial_layers'; const layerListNotProvided = undefined; describe('Saved object has layer list', () => { + beforeEach(() => { + require('../kibana_services').getInjectedVarFunc = () => jest.fn(); + }); + it('Should get initial layers from saved object', () => { const layerListFromSavedObject = [ { @@ -64,7 +65,7 @@ describe('EMS is enabled', () => { require('../meta').getKibanaTileMap = () => { return null; }; - require('ui/chrome').getInjected = key => { + require('../kibana_services').getInjectedVarFunc = () => key => { switch (key) { case 'emsTileLayerId': return { @@ -75,7 +76,7 @@ describe('EMS is enabled', () => { case 'isEmsEnabled': return true; default: - throw new Error(`Unexpected call to chrome.getInjected with key ${key}`); + throw new Error(`Unexpected call to getInjectedVarFunc with key ${key}`); } }; }); @@ -109,12 +110,12 @@ describe('EMS is not enabled', () => { return null; }; - require('ui/chrome').getInjected = key => { + require('../kibana_services').getInjectedVarFunc = () => key => { switch (key) { case 'isEmsEnabled': return false; default: - throw new Error(`Unexpected call to chrome.getInjected with key ${key}`); + throw new Error(`Unexpected call to getInjectedVarFunc with key ${key}`); } }; }); diff --git a/x-pack/legacy/plugins/maps/public/angular/map_controller.js b/x-pack/legacy/plugins/maps/public/angular/map_controller.js index 7b3dc74d777b2..519ba0b1e3d96 100644 --- a/x-pack/legacy/plugins/maps/public/angular/map_controller.js +++ b/x-pack/legacy/plugins/maps/public/angular/map_controller.js @@ -15,7 +15,7 @@ import { i18n } from '@kbn/i18n'; import { capabilities } from 'ui/capabilities'; import { render, unmountComponentAtNode } from 'react-dom'; import { uiModules } from 'ui/modules'; -import { timefilter } from 'ui/timefilter'; +import { getTimeFilter, getIndexPatternService, getInspector } from '../kibana_services'; import { Provider } from 'react-redux'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { createMapStore } from '../../../../../plugins/maps/public/reducers/store'; @@ -52,7 +52,7 @@ import { // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { getInspectorAdapters } from '../../../../../plugins/maps/public/reducers/non_serializable_instances'; import { docTitle } from 'ui/doc_title'; -import { indexPatternService, getInspector } from '../kibana_services'; + import { toastNotifications } from 'ui/notify'; import { getInitialLayers } from './get_initial_layers'; import { getInitialQuery } from './get_initial_query'; @@ -396,7 +396,7 @@ app.controller( const indexPatterns = []; const getIndexPatternPromises = nextIndexPatternIds.map(async indexPatternId => { try { - const indexPattern = await indexPatternService.get(indexPatternId); + const indexPattern = await getIndexPatternService().get(indexPatternId); indexPatterns.push(indexPattern); } catch (err) { // unable to fetch index pattern @@ -519,8 +519,8 @@ app.controller( } // Hide angular timepicer/refresh UI from top nav - timefilter.disableTimeRangeSelector(); - timefilter.disableAutoRefreshSelector(); + getTimeFilter().disableTimeRangeSelector(); + getTimeFilter().disableAutoRefreshSelector(); $scope.showDatePicker = true; // used by query-bar directive to enable timepikcer in query bar $scope.topNavMenu = [ { diff --git a/x-pack/legacy/plugins/maps/public/components/geo_field_with_index.ts b/x-pack/legacy/plugins/maps/public/components/geo_field_with_index.ts index 863e0adda8fb2..3962da23bd073 100644 --- a/x-pack/legacy/plugins/maps/public/components/geo_field_with_index.ts +++ b/x-pack/legacy/plugins/maps/public/components/geo_field_with_index.ts @@ -7,7 +7,7 @@ // Maps can contain geo fields from multiple index patterns. GeoFieldWithIndex is used to: // 1) Combine the geo field along with associated index pattern state. -// 2) Package asynchronously looked up state via indexPatternService to avoid +// 2) Package asynchronously looked up state via getIndexPatternService() to avoid // PITA of looking up async state in downstream react consumers. export type GeoFieldWithIndex = { geoFieldName: string; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/filter_editor/filter_editor.js b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/filter_editor/filter_editor.js index 60bbaa9825db7..f6bcac0dfc339 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/filter_editor/filter_editor.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/filter_editor/filter_editor.js @@ -20,7 +20,7 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { indexPatternService } from '../../../kibana_services'; +import { getIndexPatternService } from '../../../kibana_services'; import { GlobalFilterCheckbox } from '../../../components/global_filter_checkbox'; import { npStart } from 'ui/new_platform'; @@ -47,7 +47,7 @@ export class FilterEditor extends Component { const indexPatterns = []; const getIndexPatternPromises = indexPatternIds.map(async indexPatternId => { try { - const indexPattern = await indexPatternService.get(indexPatternId); + const indexPattern = await getIndexPatternService().get(indexPatternId); indexPatterns.push(indexPattern); } catch (err) { // unable to fetch index pattern diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/join.js b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/join.js index c2c9f333a675c..0df6bd40d1a31 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/join.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/join.js @@ -14,7 +14,7 @@ import { WhereExpression } from './where_expression'; import { GlobalFilterCheckbox } from '../../../../components/global_filter_checkbox'; import { indexPatterns } from '../../../../../../../../../src/plugins/data/public'; -import { indexPatternService } from '../../../../kibana_services'; +import { getIndexPatternService } from '../../../../kibana_services'; export class Join extends Component { state = { @@ -39,7 +39,7 @@ export class Join extends Component { let indexPattern; try { - indexPattern = await indexPatternService.get(indexPatternId); + indexPattern = await getIndexPatternService().get(indexPatternId); } catch (err) { if (this._isMounted) { this.setState({ diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/join_expression.js b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/join_expression.js index 777c8ae0923fe..f7edcf6e85e25 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/join_expression.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/join_expression.js @@ -19,11 +19,10 @@ import { i18n } from '@kbn/i18n'; import { SingleFieldSelect } from '../../../../components/single_field_select'; import { FormattedMessage } from '@kbn/i18n/react'; import { getTermsFields } from '../../../../index_pattern_util'; - -import { indexPatternService } from '../../../../kibana_services'; - -import { npStart } from 'ui/new_platform'; -const { IndexPatternSelect } = npStart.plugins.data.ui; +import { + getIndexPatternService, + getIndexPatternSelectComponent, +} from '../../../../kibana_services'; export class JoinExpression extends Component { state = { @@ -44,7 +43,7 @@ export class JoinExpression extends Component { _onRightSourceChange = async indexPatternId => { try { - const indexPattern = await indexPatternService.get(indexPatternId); + const indexPattern = await getIndexPatternService().get(indexPatternId); this.props.onRightSourceChange({ indexPatternId, indexPatternTitle: indexPattern.title, @@ -106,6 +105,7 @@ export class JoinExpression extends Component { if (!this.props.leftValue) { return null; } + const IndexPatternSelect = getIndexPatternSelectComponent(); return ( APP_ICON, }, }); + // Init required services. Necessary while in legacy bindSetupCoreAndPlugins(npSetup.core, npSetup.plugins); + bindStartCoreAndPlugins(npStart.core, npStart.plugins); } isEditable() { return capabilities.get().maps.save; @@ -76,7 +78,7 @@ export class MapEmbeddableFactory extends EmbeddableFactory { const promises = queryableIndexPatternIds.map(async indexPatternId => { try { - return await indexPatternService.get(indexPatternId); + return await getIndexPatternService().get(indexPatternId); } catch (error) { // Unable to load index pattern, better to not throw error so map embeddable can render // Error will be surfaced by map embeddable since it too will be unable to locate the index pattern diff --git a/x-pack/legacy/plugins/maps/public/index_pattern_util.js b/x-pack/legacy/plugins/maps/public/index_pattern_util.js index 7aa87ab32cdf5..30a0a6826db83 100644 --- a/x-pack/legacy/plugins/maps/public/index_pattern_util.js +++ b/x-pack/legacy/plugins/maps/public/index_pattern_util.js @@ -4,14 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { indexPatternService } from './kibana_services'; +import { getIndexPatternService } from './kibana_services'; import { indexPatterns } from '../../../../../src/plugins/data/public'; import { ES_GEO_FIELD_TYPE } from '../common/constants'; export async function getIndexPatternsFromIds(indexPatternIds = []) { const promises = []; indexPatternIds.forEach(id => { - const indexPatternPromise = indexPatternService.get(id); + const indexPatternPromise = getIndexPatternService().get(id); if (indexPatternPromise) { promises.push(indexPatternPromise); } diff --git a/x-pack/legacy/plugins/maps/public/kibana_services.js b/x-pack/legacy/plugins/maps/public/kibana_services.js index 5702eb1c6f846..3b0f501dc0f60 100644 --- a/x-pack/legacy/plugins/maps/public/kibana_services.js +++ b/x-pack/legacy/plugins/maps/public/kibana_services.js @@ -6,12 +6,18 @@ import { esFilters, search } from '../../../../../src/plugins/data/public'; const { getRequestInspectorStats, getResponseInspectorStats } = search; -import { npStart } from 'ui/new_platform'; export const SPATIAL_FILTER_TYPE = esFilters.FILTERS.SPATIAL_FILTER; export { SearchSource } from '../../../../../src/plugins/data/public'; -export const indexPatternService = npStart.plugins.data.indexPatterns; -export const autocompleteService = npStart.plugins.data.autocomplete; + +let indexPatternService; +export const setIndexPatternService = dataIndexPatterns => + (indexPatternService = dataIndexPatterns); +export const getIndexPatternService = () => indexPatternService; + +let autocompleteService; +export const setAutocompleteService = dataAutoComplete => (autocompleteService = dataAutoComplete); +export const getAutocompleteService = () => autocompleteService; let licenseId; export const setLicenseId = latestLicenseId => (licenseId = latestLicenseId); @@ -31,6 +37,31 @@ export const getFileUploadComponent = () => { return fileUploadPlugin.JsonUploadAndParse; }; +let getInjectedVar; +export const setInjectedVarFunc = getInjectedVarFunc => (getInjectedVar = getInjectedVarFunc); +export const getInjectedVarFunc = () => getInjectedVar; + +let uiSettings; +export const setUiSettings = coreUiSettings => (uiSettings = coreUiSettings); +export const getUiSettings = () => uiSettings; + +let indexPatternSelectComponent; +export const setIndexPatternSelect = indexPatternSelect => + (indexPatternSelectComponent = indexPatternSelect); +export const getIndexPatternSelectComponent = () => indexPatternSelectComponent; + +let coreHttp; +export const setHttp = http => (coreHttp = http); +export const getHttp = () => coreHttp; + +let dataTimeFilter; +export const setTimeFilter = timeFilter => (dataTimeFilter = timeFilter); +export const getTimeFilter = () => dataTimeFilter; + +let toast; +export const setToasts = notificationToast => (toast = notificationToast); +export const getToasts = () => toast; + export async function fetchSearchSourceAndRecordWithInspector({ searchSource, requestId, diff --git a/x-pack/legacy/plugins/maps/public/layers/joins/inner_join.test.js b/x-pack/legacy/plugins/maps/public/layers/joins/inner_join.test.js index 4a91ed3a3eafb..65c37860ffa18 100644 --- a/x-pack/legacy/plugins/maps/public/layers/joins/inner_join.test.js +++ b/x-pack/legacy/plugins/maps/public/layers/joins/inner_join.test.js @@ -7,7 +7,6 @@ import { InnerJoin } from './inner_join'; jest.mock('../../kibana_services', () => {}); -jest.mock('ui/timefilter', () => {}); jest.mock('../vector_layer', () => {}); const rightSource = { diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/ems_tms_source/ems_tms_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/ems_tms_source/ems_tms_source.js index 76ecc18f2f7d7..5a2124622694c 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/ems_tms_source/ems_tms_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/ems_tms_source/ems_tms_source.js @@ -5,7 +5,6 @@ */ import _ from 'lodash'; -import chrome from 'ui/chrome'; import React from 'react'; import { AbstractTMSSource } from '../tms_source'; import { VectorTileLayer } from '../../vector_tile_layer'; @@ -16,6 +15,7 @@ import { UpdateSourceEditor } from './update_source_editor'; import { i18n } from '@kbn/i18n'; import { getDataSourceLabel } from '../../../../common/i18n_getters'; import { EMS_TMS } from '../../../../common/constants'; +import { getInjectedVarFunc, getUiSettings } from '../../../kibana_services'; export class EMSTMSSource extends AbstractTMSSource { static type = EMS_TMS; @@ -152,8 +152,8 @@ export class EMSTMSSource extends AbstractTMSSource { return this._descriptor.id; } - const isDarkMode = chrome.getUiSettingsClient().get('theme:darkMode', false); - const emsTileLayerId = chrome.getInjected('emsTileLayerId'); + const isDarkMode = getUiSettings().get('theme:darkMode', false); + const emsTileLayerId = getInjectedVarFunc()('emsTileLayerId'); return isDarkMode ? emsTileLayerId.dark : emsTileLayerId.bright; } } diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/ems_unavailable_message.js b/x-pack/legacy/plugins/maps/public/layers/sources/ems_unavailable_message.js index 22b1088047539..bc50890a0f4a3 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/ems_unavailable_message.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/ems_unavailable_message.js @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import chrome from 'ui/chrome'; +import { getInjectedVarFunc } from '../../kibana_services'; import { i18n } from '@kbn/i18n'; export function getEmsUnavailableMessage() { - const isEmsEnabled = chrome.getInjected('isEmsEnabled', true); + const isEmsEnabled = getInjectedVarFunc()('isEmsEnabled', true); if (isEmsEnabled) { return i18n.translate('xpack.maps.source.ems.noAccessDescription', { defaultMessage: diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/create_source_editor.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/create_source_editor.js index 00cbfbbb6c5a7..148683269ef78 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/create_source_editor.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/create_source_editor.js @@ -10,7 +10,7 @@ import PropTypes from 'prop-types'; import { SingleFieldSelect } from '../../../components/single_field_select'; import { RENDER_AS } from '../../../../common/constants'; -import { indexPatternService } from '../../../kibana_services'; +import { getIndexPatternService, getIndexPatternSelectComponent } from '../../../kibana_services'; import { NoIndexPatternCallout } from '../../../components/no_index_pattern_callout'; import { i18n } from '@kbn/i18n'; @@ -20,9 +20,6 @@ import { getAggregatableGeoFields, } from '../../../index_pattern_util'; -import { npStart } from 'ui/new_platform'; -const { IndexPatternSelect } = npStart.plugins.data.ui; - const requestTypeOptions = [ { label: i18n.translate('xpack.maps.source.esGeoGrid.gridRectangleDropdownOption', { @@ -92,7 +89,7 @@ export class CreateSourceEditor extends Component { let indexPattern; try { - indexPattern = await indexPatternService.get(indexPatternId); + indexPattern = await getIndexPatternService().get(indexPatternId); } catch (err) { // index pattern no longer exists return; @@ -205,6 +202,8 @@ export class CreateSourceEditor extends Component { } _renderIndexPatternSelect() { + const IndexPatternSelect = getIndexPatternSelectComponent(); + return ( { return ( @@ -81,8 +81,10 @@ export class CreateSourceEditor extends Component { }; loadIndexDocCount = async indexPatternTitle => { - const { count } = await kfetch({ - pathname: `../${GIS_API_PATH}/indexCount`, + const http = getHttp(); + const { count } = await http.fetch(`../${GIS_API_PATH}/indexCount`, { + method: 'GET', + credentials: 'same-origin', query: { index: indexPatternTitle, }, @@ -97,7 +99,7 @@ export class CreateSourceEditor extends Component { let indexPattern; try { - indexPattern = await indexPatternService.get(indexPatternId); + indexPattern = await getIndexPatternService().get(indexPatternId); } catch (err) { // index pattern no longer exists return; @@ -249,6 +251,8 @@ export class CreateSourceEditor extends Component { } render() { + const IndexPatternSelect = getIndexPatternSelectComponent(); + return ( {this._renderNoIndexPatternWarning()} diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/es_search_source.test.ts b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/es_search_source.test.ts index 59120e221ca49..2197e24aedb59 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/es_search_source.test.ts +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/es_search_source.test.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ jest.mock('ui/new_platform'); +jest.mock('../../../kibana_services'); import { ESSearchSource } from './es_search_source'; import { VectorLayer } from '../../vector_layer'; @@ -19,6 +20,11 @@ const descriptor: ESSearchSourceDescriptor = { }; describe('ES Search Source', () => { + beforeEach(() => { + require('../../../kibana_services').getUiSettings = () => ({ + get: jest.fn(), + }); + }); it('should create a vector layer', () => { const source = new ESSearchSource(descriptor, null); const layer = source.createDefaultLayer(); diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/load_index_settings.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/load_index_settings.js index 1a58b5b073b08..811291de26d35 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/load_index_settings.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/load_index_settings.js @@ -9,8 +9,7 @@ import { DEFAULT_MAX_INNER_RESULT_WINDOW, INDEX_SETTINGS_API_PATH, } from '../../../../common/constants'; -import { kfetch } from 'ui/kfetch'; -import { toastNotifications } from 'ui/notify'; +import { getHttp, getToasts } from '../../../kibana_services'; import { i18n } from '@kbn/i18n'; let toastDisplayed = false; @@ -27,9 +26,12 @@ export async function loadIndexSettings(indexPatternTitle) { } async function fetchIndexSettings(indexPatternTitle) { + const http = getHttp(); + const toasts = getToasts(); try { - const indexSettings = await kfetch({ - pathname: `../${INDEX_SETTINGS_API_PATH}`, + const indexSettings = await http.fetch(`../${INDEX_SETTINGS_API_PATH}`, { + method: 'GET', + credentials: 'same-origin', query: { indexPatternTitle, }, @@ -47,7 +49,7 @@ async function fetchIndexSettings(indexPatternTitle) { if (!toastDisplayed) { // Only show toast for first failure to avoid flooding user with warnings toastDisplayed = true; - toastNotifications.addWarning(warningMsg); + toasts.addWarning(warningMsg); } console.warn(warningMsg); return { diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/update_source_editor.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/update_source_editor.js index b85cca113cf98..4d1e32087ab8c 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/update_source_editor.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/update_source_editor.js @@ -19,7 +19,7 @@ import { import { SingleFieldSelect } from '../../../components/single_field_select'; import { TooltipSelector } from '../../../components/tooltip_selector'; -import { indexPatternService } from '../../../kibana_services'; +import { getIndexPatternService } from '../../../kibana_services'; import { i18n } from '@kbn/i18n'; import { getTermsFields, getSourceFields } from '../../../index_pattern_util'; import { ValidatedRange } from '../../../components/validated_range'; @@ -69,7 +69,7 @@ export class UpdateSourceEditor extends Component { async loadIndexSettings() { try { - const indexPattern = await indexPatternService.get(this.props.indexPatternId); + const indexPattern = await getIndexPatternService().get(this.props.indexPatternId); const { maxInnerResultWindow, maxResultWindow } = await loadIndexSettings(indexPattern.title); if (this._isMounted) { this.setState({ maxInnerResultWindow, maxResultWindow }); @@ -82,7 +82,7 @@ export class UpdateSourceEditor extends Component { async loadFields() { let indexPattern; try { - indexPattern = await indexPatternService.get(this.props.indexPatternId); + indexPattern = await getIndexPatternService().get(this.props.indexPatternId); } catch (err) { if (this._isMounted) { this.setState({ diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_source.js index c5bf9a8be75bd..8b079b5202f7f 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_source.js @@ -6,13 +6,13 @@ import { AbstractVectorSource } from './vector_source'; import { - autocompleteService, + getAutocompleteService, fetchSearchSourceAndRecordWithInspector, - indexPatternService, + getIndexPatternService, SearchSource, + getTimeFilter, } from '../../kibana_services'; import { createExtentFilter } from '../../elasticsearch_geo_utils'; -import { timefilter } from 'ui/timefilter'; import _ from 'lodash'; import { i18n } from '@kbn/i18n'; import uuid from 'uuid/v4'; @@ -125,7 +125,7 @@ export class AbstractESSource extends AbstractVectorSource { allFilters.push(createExtentFilter(buffer, geoField.name, geoField.type)); } if (isTimeAware) { - allFilters.push(timefilter.createFilter(indexPattern, searchFilters.timeFilters)); + allFilters.push(getTimeFilter().createFilter(indexPattern, searchFilters.timeFilters)); } const searchSource = new SearchSource(initialSearchContext); @@ -208,7 +208,7 @@ export class AbstractESSource extends AbstractVectorSource { } try { - this.indexPattern = await indexPatternService.get(this.getIndexPatternId()); + this.indexPattern = await getIndexPatternService().get(this.getIndexPatternId()); return this.indexPattern; } catch (error) { throw new Error( @@ -305,7 +305,7 @@ export class AbstractESSource extends AbstractVectorSource { } if (style.isTimeAware() && (await this.isTimeAware())) { searchSource.setField('filter', [ - timefilter.createFilter(indexPattern, searchFilters.timeFilters), + getTimeFilter().createFilter(indexPattern, searchFilters.timeFilters), ]); } @@ -332,7 +332,7 @@ export class AbstractESSource extends AbstractVectorSource { getValueSuggestions = async (field, query) => { try { const indexPattern = await this.getIndexPattern(); - return await autocompleteService.getValueSuggestions({ + return await getAutocompleteService().getValueSuggestions({ indexPattern, field: indexPattern.fields.getByName(field.getRootName()), query, diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_term_source.test.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_term_source.test.js index 890b1e3aaac1f..14ffd068df465 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_term_source.test.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_term_source.test.js @@ -8,7 +8,6 @@ import { ESTermSource, extractPropertiesMap } from './es_term_source'; jest.mock('ui/new_platform'); jest.mock('../vector_layer', () => {}); -jest.mock('ui/timefilter', () => {}); const indexPatternTitle = 'myIndex'; const termFieldName = 'myTermField'; diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/color_utils.js b/x-pack/legacy/plugins/maps/public/layers/styles/color_utils.js index fc305f8daed59..a619eaba21aef 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/color_utils.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/color_utils.js @@ -7,11 +7,7 @@ import React from 'react'; import tinycolor from 'tinycolor2'; import chroma from 'chroma-js'; - import { euiPaletteColorBlind } from '@elastic/eui/lib/services'; - -import { getLegendColors, getColor } from 'ui/vis/map/color_util'; - import { ColorGradient } from './components/color_gradient'; import { COLOR_PALETTE_MAX_SIZE } from '../../../common/constants'; import { vislibColorMaps } from '../../../../../../../src/plugins/charts/public'; @@ -30,6 +26,24 @@ export const DEFAULT_LINE_COLORS = [ '#FFF', ]; +function getLegendColors(colorRamp, numLegendColors = 4) { + const colors = []; + colors[0] = getColor(colorRamp, 0); + for (let i = 1; i < numLegendColors - 1; i++) { + colors[i] = getColor(colorRamp, Math.floor((colorRamp.length * i) / numLegendColors)); + } + colors[numLegendColors - 1] = getColor(colorRamp, colorRamp.length - 1); + return colors; +} + +function getColor(colorRamp, i) { + const color = colorRamp[i][1]; + const red = Math.floor(color[0] * 255); + const green = Math.floor(color[1] * 255); + const blue = Math.floor(color[2] * 255); + return `rgb(${red},${green},${blue})`; +} + function getColorRamp(colorRampName) { const colorRamp = vislibColorMaps[colorRampName]; if (!colorRamp) { diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/symbol/vector_style_icon_editor.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/symbol/vector_style_icon_editor.js index d5ec09f515954..36b6c1a76470c 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/symbol/vector_style_icon_editor.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/symbol/vector_style_icon_editor.js @@ -6,7 +6,7 @@ import React from 'react'; -import chrome from 'ui/chrome'; +import { getUiSettings } from '../../../../../kibana_services'; import { StylePropEditor } from '../style_prop_editor'; import { DynamicIconForm } from './dynamic_icon_form'; import { StaticIconForm } from './static_icon_form'; @@ -16,13 +16,13 @@ export function VectorStyleIconEditor(props) { const iconForm = props.styleProperty.isDynamic() ? ( ) : ( ); diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.test.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.test.js index 66b7ae5e02c5f..b3f653a70f472 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.test.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.test.js @@ -9,6 +9,7 @@ import { DataRequest } from '../../util/data_request'; import { VECTOR_SHAPE_TYPES } from '../../sources/vector_feature_types'; import { FIELD_ORIGIN } from '../../../../common/constants'; +jest.mock('../../../kibana_services'); jest.mock('ui/new_platform'); class MockField { @@ -65,7 +66,13 @@ describe('getDescriptorWithMissingStylePropsRemoved', () => { }, }; - it('Should return no changes when next oridinal fields contain existing style property fields', () => { + beforeEach(() => { + require('../../../kibana_services').getUiSettings = () => ({ + get: jest.fn(), + }); + }); + + it('Should return no changes when next ordinal fields contain existing style property fields', () => { const vectorStyle = new VectorStyle({ properties }, new MockSource()); const nextFields = [new MockField({ fieldName })]; @@ -73,7 +80,7 @@ describe('getDescriptorWithMissingStylePropsRemoved', () => { expect(hasChanges).toBe(false); }); - it('Should clear missing fields when next oridinal fields do not contain existing style property fields', () => { + it('Should clear missing fields when next ordinal fields do not contain existing style property fields', () => { const vectorStyle = new VectorStyle({ properties }, new MockSource()); const nextFields = []; diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style_defaults.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style_defaults.js index dd2cf79318d8e..fdfd71d240989 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style_defaults.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style_defaults.js @@ -12,7 +12,7 @@ import { DEFAULT_FILL_COLORS, DEFAULT_LINE_COLORS, } from '../color_utils'; -import chrome from 'ui/chrome'; +import { getUiSettings } from '../../../kibana_services'; export const MIN_SIZE = 1; export const MAX_SIZE = 64; @@ -67,7 +67,7 @@ export function getDefaultStaticProperties(mapColors = []) { const nextFillColor = DEFAULT_FILL_COLORS[nextColorIndex]; const nextLineColor = DEFAULT_LINE_COLORS[nextColorIndex]; - const isDarkMode = chrome.getUiSettingsClient().get('theme:darkMode', false); + const isDarkMode = getUiSettings().get('theme:darkMode', false); return { [VECTOR_STYLES.ICON]: { diff --git a/x-pack/legacy/plugins/maps/public/legacy.ts b/x-pack/legacy/plugins/maps/public/legacy.ts index 6adab529daf86..96d9e09c1d09a 100644 --- a/x-pack/legacy/plugins/maps/public/legacy.ts +++ b/x-pack/legacy/plugins/maps/public/legacy.ts @@ -19,9 +19,5 @@ const setupPlugins = { np: npSetup.plugins, }; -const startPlugins = { - np: npStart.plugins, -}; - export const setup = pluginInstance.setup(npSetup.core, setupPlugins); -export const start = pluginInstance.start(npStart.core, startPlugins); +export const start = pluginInstance.start(npStart.core, npStart.plugins); diff --git a/x-pack/legacy/plugins/maps/public/plugin.ts b/x-pack/legacy/plugins/maps/public/plugin.ts index e2d1d43295646..1f8f83e44a769 100644 --- a/x-pack/legacy/plugins/maps/public/plugin.ts +++ b/x-pack/legacy/plugins/maps/public/plugin.ts @@ -8,14 +8,33 @@ import { Plugin, CoreStart, CoreSetup } from 'src/core/public'; // @ts-ignore import { wrapInI18nContext } from 'ui/i18n'; // @ts-ignore -import { MapListing } from './components/map_listing'; +import { Start as InspectorStartContract } from 'src/plugins/inspector/public'; // @ts-ignore -import { setInjectedVarFunc } from '../../../../plugins/maps/public/kibana_services'; // eslint-disable-line @kbn/eslint/no-restricted-paths +import { MapListing } from './components/map_listing'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { + setLicenseId, + setInspector, + setFileUpload, + setIndexPatternSelect, + setHttp, + setTimeFilter, + setUiSettings, + setInjectedVarFunc, + setToasts, + setIndexPatternService, + setAutocompleteService, + // @ts-ignore +} from './kibana_services'; // @ts-ignore -import { setLicenseId, setInspector, setFileUpload } from './kibana_services'; +import { setInjectedVarFunc as npSetInjectedVarFunc } from '../../../../plugins/maps/public/kibana_services'; // eslint-disable-line @kbn/eslint/no-restricted-paths import { HomePublicPluginSetup } from '../../../../../src/plugins/home/public'; import { LicensingPluginSetup } from '../../../../plugins/licensing/public'; import { featureCatalogueEntry } from './feature_catalogue_entry'; +import { + DataPublicPluginSetup, + DataPublicPluginStart, +} from '../../../../../src/plugins/data/public'; /** * These are the interfaces with your public contracts. You should export these @@ -30,16 +49,38 @@ interface MapsPluginSetupDependencies { np: { licensing?: LicensingPluginSetup; home: HomePublicPluginSetup; + data: DataPublicPluginSetup; }; } +interface MapsPluginStartDependencies { + data: DataPublicPluginStart; + inspector: InspectorStartContract; + // file_upload TODO: Export type from file upload and use here +} + export const bindSetupCoreAndPlugins = (core: CoreSetup, plugins: any) => { const { licensing } = plugins; - const { injectedMetadata } = core; + const { injectedMetadata, http } = core; if (licensing) { licensing.license$.subscribe(({ uid }: { uid: string }) => setLicenseId(uid)); } setInjectedVarFunc(injectedMetadata.getInjectedVar); + setHttp(http); + setUiSettings(core.uiSettings); + setInjectedVarFunc(core.injectedMetadata.getInjectedVar); + npSetInjectedVarFunc(core.injectedMetadata.getInjectedVar); + setToasts(core.notifications.toasts); +}; + +export const bindStartCoreAndPlugins = (core: CoreStart, plugins: any) => { + const { file_upload, data, inspector } = plugins; + setInspector(inspector); + setFileUpload(file_upload); + setIndexPatternSelect(data.ui.IndexPatternSelect); + setTimeFilter(data.query.timefilter.timefilter); + setIndexPatternService(data.indexPatterns); + setAutocompleteService(data.autocompleteService); }; /** @internal */ @@ -56,9 +97,7 @@ export class MapsPlugin implements Plugin { np.home.featureCatalogue.register(featureCatalogueEntry); } - public start(core: CoreStart, plugins: any) { - const { inspector, file_upload } = plugins.np; - setInspector(inspector); - setFileUpload(file_upload); + public start(core: CoreStart, plugins: MapsPluginStartDependencies) { + bindStartCoreAndPlugins(core, plugins); } } diff --git a/x-pack/legacy/plugins/maps/public/selectors/map_selectors.js b/x-pack/legacy/plugins/maps/public/selectors/map_selectors.js index 79d890bc21f14..61eea2d172ae4 100644 --- a/x-pack/legacy/plugins/maps/public/selectors/map_selectors.js +++ b/x-pack/legacy/plugins/maps/public/selectors/map_selectors.js @@ -12,7 +12,7 @@ import { VectorLayer } from '../layers/vector_layer'; import { HeatmapLayer } from '../layers/heatmap_layer'; import { BlendedVectorLayer } from '../layers/blended_vector_layer'; import { ALL_SOURCES } from '../layers/sources/all_sources'; -import { timefilter } from 'ui/timefilter'; +import { getTimeFilter } from '../kibana_services'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { getInspectorAdapters } from '../../../../../plugins/maps/public/reducers/non_serializable_instances'; import { @@ -109,7 +109,7 @@ export const getMapCenter = ({ map }) => export const getMouseCoordinates = ({ map }) => map.mapState.mouseCoordinates; export const getTimeFilters = ({ map }) => - map.mapState.timeFilters ? map.mapState.timeFilters : timefilter.getTime(); + map.mapState.timeFilters ? map.mapState.timeFilters : getTimeFilter().getTime(); export const getQuery = ({ map }) => map.mapState.query; @@ -132,7 +132,7 @@ export const getRefreshConfig = ({ map }) => { return map.mapState.refreshConfig; } - const refreshInterval = timefilter.getRefreshInterval(); + const refreshInterval = getTimeFilter().getRefreshInterval(); return { isPaused: refreshInterval.pause, interval: refreshInterval.value, diff --git a/x-pack/legacy/plugins/maps/public/selectors/map_selectors.test.js b/x-pack/legacy/plugins/maps/public/selectors/map_selectors.test.js index ef2e23e51a092..e7f071d5729c6 100644 --- a/x-pack/legacy/plugins/maps/public/selectors/map_selectors.test.js +++ b/x-pack/legacy/plugins/maps/public/selectors/map_selectors.test.js @@ -15,15 +15,15 @@ jest.mock('../../../../../plugins/maps/public/reducers/non_serializable_instance return {}; }, })); -jest.mock('ui/timefilter', () => ({ - timefilter: { +jest.mock('../kibana_services', () => ({ + getTimeFilter: () => ({ getTime: () => { return { to: 'now', from: 'now-15m', }; }, - }, + }), })); import { getTimeFilters } from './map_selectors'; From 992c502cf5ed377ce4532397a037bae695121d14 Mon Sep 17 00:00:00 2001 From: Dmitry Lemeshko Date: Fri, 20 Mar 2020 17:18:35 +0300 Subject: [PATCH 3/9] WebElementWrapper: add findByTestSubject/findAllByTestSubject to search with data-test-subj (#60568) * [web_element_wrapper] add find/findAll to search with data-test-subj * fixes * fix wrong function call * review fixes * simplify test Co-authored-by: Elastic Machine --- test/functional/apps/visualize/_tile_map.js | 3 +- test/functional/page_objects/common_page.ts | 2 +- test/functional/page_objects/discover_page.ts | 4 +-- test/functional/page_objects/newsfeed_page.ts | 2 +- test/functional/page_objects/settings_page.ts | 3 +- test/functional/page_objects/timelion_page.js | 5 ++- .../page_objects/visual_builder_page.ts | 4 +-- .../page_objects/visualize_editor_page.ts | 2 +- test/functional/services/combo_box.ts | 10 +++--- test/functional/services/doc_table.ts | 18 +++++----- .../web_element_wrapper.ts | 34 +++++++++++++++++++ .../test/functional/page_objects/gis_page.js | 2 +- .../functional/page_objects/rollup_page.js | 30 ++++++---------- .../functional/page_objects/security_page.js | 27 ++++++--------- .../page_objects/snapshot_restore_page.ts | 18 ++++------ .../apps/triggers_actions_ui/alerts.ts | 10 ++---- .../page_objects/triggers_actions_ui_page.ts | 4 +-- 17 files changed, 90 insertions(+), 88 deletions(-) diff --git a/test/functional/apps/visualize/_tile_map.js b/test/functional/apps/visualize/_tile_map.js index ee07e66757b6f..9e39e93926c95 100644 --- a/test/functional/apps/visualize/_tile_map.js +++ b/test/functional/apps/visualize/_tile_map.js @@ -23,7 +23,6 @@ export default function({ getService, getPageObjects }) { const log = getService('log'); const retry = getService('retry'); const inspector = getService('inspector'); - const find = getService('find'); const filterBar = getService('filterBar'); const testSubjects = getService('testSubjects'); const browser = getService('browser'); @@ -279,7 +278,7 @@ export default function({ getService, getPageObjects }) { it('should suppress zoom warning if suppress warnings button clicked', async () => { last = true; await PageObjects.visChart.waitForVisualization(); - await find.clickByCssSelector('[data-test-subj="suppressZoomWarnings"]'); + await testSubjects.click('suppressZoomWarnings'); await PageObjects.tileMap.clickMapZoomOut(waitForLoading); await testSubjects.waitForDeleted('suppressZoomWarnings'); await PageObjects.tileMap.clickMapZoomIn(waitForLoading); diff --git a/test/functional/page_objects/common_page.ts b/test/functional/page_objects/common_page.ts index 6895034f22ed5..de4917ef2b1b3 100644 --- a/test/functional/page_objects/common_page.ts +++ b/test/functional/page_objects/common_page.ts @@ -100,7 +100,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo private async loginIfPrompted(appUrl: string) { let currentUrl = await browser.getCurrentUrl(); log.debug(`currentUrl = ${currentUrl}\n appUrl = ${appUrl}`); - await find.byCssSelector('[data-test-subj="kibanaChrome"]', 6 * defaultFindTimeout); // 60 sec waiting + await testSubjects.find('kibanaChrome', 6 * defaultFindTimeout); // 60 sec waiting const loginPage = currentUrl.includes('/login'); const wantedLoginPage = appUrl.includes('/login') || appUrl.includes('/logout'); diff --git a/test/functional/page_objects/discover_page.ts b/test/functional/page_objects/discover_page.ts index f018a1ceda507..a126cfb1bce4b 100644 --- a/test/functional/page_objects/discover_page.ts +++ b/test/functional/page_objects/discover_page.ts @@ -255,14 +255,14 @@ export function DiscoverPageProvider({ getService, getPageObjects }: FtrProvider public async clickFieldListPlusFilter(field: string, value: string) { // this method requires the field details to be open from clickFieldListItem() // testSubjects.find doesn't handle spaces in the data-test-subj value - await find.clickByCssSelector(`[data-test-subj="plus-${field}-${value}"]`); + await testSubjects.click(`plus-${field}-${value}`); await header.waitUntilLoadingHasFinished(); } public async clickFieldListMinusFilter(field: string, value: string) { // this method requires the field details to be open from clickFieldListItem() // testSubjects.find doesn't handle spaces in the data-test-subj value - await find.clickByCssSelector('[data-test-subj="minus-' + field + '-' + value + '"]'); + await testSubjects.click(`minus-${field}-${value}`); await header.waitUntilLoadingHasFinished(); } diff --git a/test/functional/page_objects/newsfeed_page.ts b/test/functional/page_objects/newsfeed_page.ts index 24ff21f0b47de..ade3bdadf25d4 100644 --- a/test/functional/page_objects/newsfeed_page.ts +++ b/test/functional/page_objects/newsfeed_page.ts @@ -54,7 +54,7 @@ export function NewsfeedPageProvider({ getService, getPageObjects }: FtrProvider async getNewsfeedList() { const list = await testSubjects.find('NewsfeedFlyout'); - const cells = await list.findAllByCssSelector('[data-test-subj="newsHeadAlert"]'); + const cells = await list.findAllByTestSubject('newsHeadAlert'); const objects = []; for (const cell of cells) { diff --git a/test/functional/page_objects/settings_page.ts b/test/functional/page_objects/settings_page.ts index 0ad1a1dc51321..e0f64340ca7dc 100644 --- a/test/functional/page_objects/settings_page.ts +++ b/test/functional/page_objects/settings_page.ts @@ -211,9 +211,8 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider } async getScriptedFieldsTabCount() { - const selector = '[data-test-subj="tab-count-scriptedFields"]'; return await retry.try(async () => { - const theText = await (await find.byCssSelector(selector)).getVisibleText(); + const theText = await testSubjects.getVisibleText('tab-count-scriptedFields'); return theText.replace(/\((.*)\)/, '$1'); }); } diff --git a/test/functional/page_objects/timelion_page.js b/test/functional/page_objects/timelion_page.js index 4aaa654e4286a..88eda5da5ce15 100644 --- a/test/functional/page_objects/timelion_page.js +++ b/test/functional/page_objects/timelion_page.js @@ -19,7 +19,6 @@ export function TimelionPageProvider({ getService, getPageObjects }) { const testSubjects = getService('testSubjects'); - const find = getService('find'); const log = getService('log'); const PageObjects = getPageObjects(['common', 'header']); const esArchiver = getService('esArchiver'); @@ -55,12 +54,12 @@ export function TimelionPageProvider({ getService, getPageObjects }) { } async getSuggestionItemsText() { - const elements = await find.allByCssSelector('[data-test-subj="timelionSuggestionListItem"]'); + const elements = await testSubjects.findAll('timelionSuggestionListItem'); return await Promise.all(elements.map(async element => await element.getVisibleText())); } async clickSuggestion(suggestionIndex = 0, waitTime = 500) { - const elements = await find.allByCssSelector('[data-test-subj="timelionSuggestionListItem"]'); + const elements = await testSubjects.findAll('timelionSuggestionListItem'); if (suggestionIndex > elements.length) { throw new Error( `Unable to select suggestion ${suggestionIndex}, only ${elements.length} suggestions available.` diff --git a/test/functional/page_objects/visual_builder_page.ts b/test/functional/page_objects/visual_builder_page.ts index ee0cafb51d455..0bfd2141be03e 100644 --- a/test/functional/page_objects/visual_builder_page.ts +++ b/test/functional/page_objects/visual_builder_page.ts @@ -485,7 +485,7 @@ export function VisualBuilderPageProvider({ getService, getPageObjects }: FtrPro const labels = await testSubjects.findAll('aggRow'); const label = labels[aggNth]; - return (await label.findAllByCssSelector('[data-test-subj = "comboBoxInput"]'))[1]; + return (await label.findAllByTestSubject('comboBoxInput'))[1]; } public async clickColorPicker(): Promise { @@ -533,7 +533,7 @@ export function VisualBuilderPageProvider({ getService, getPageObjects }: FtrPro */ public async getAggregationCount(nth: number = 0): Promise { const series = await this.getSeries(); - const aggregation = await series[nth].findAllByCssSelector('[data-test-subj="draggable"]'); + const aggregation = await series[nth].findAllByTestSubject('draggable'); return aggregation.length; } diff --git a/test/functional/page_objects/visualize_editor_page.ts b/test/functional/page_objects/visualize_editor_page.ts index cdc16babc4189..b1c3e924b3c1b 100644 --- a/test/functional/page_objects/visualize_editor_page.ts +++ b/test/functional/page_objects/visualize_editor_page.ts @@ -110,7 +110,7 @@ export function VisualizeEditorPageProvider({ getService, getPageObjects }: FtrP */ public async clickBucket(bucketName: string, type = 'buckets') { await testSubjects.click(`visEditorAdd_${type}`); - await find.clickByCssSelector(`[data-test-subj="visEditorAdd_${type}_${bucketName}"`); + await testSubjects.click(`visEditorAdd_${type}_${bucketName}`); } public async clickEnableCustomRanges() { diff --git a/test/functional/services/combo_box.ts b/test/functional/services/combo_box.ts index 33610e64f1c79..2c12490ccd436 100644 --- a/test/functional/services/combo_box.ts +++ b/test/functional/services/combo_box.ts @@ -218,7 +218,7 @@ export function ComboBoxProvider({ getService, getPageObjects }: FtrProviderCont return; } - const clearBtn = await comboBox.findByCssSelector('[data-test-subj="comboBoxClearButton"]'); + const clearBtn = await comboBox.findByTestSubject('comboBoxClearButton'); await clearBtn.click(); const clearButtonStillExists = await this.doesClearButtonExist(comboBox); @@ -230,8 +230,8 @@ export function ComboBoxProvider({ getService, getPageObjects }: FtrProviderCont } public async doesClearButtonExist(comboBoxElement: WebElementWrapper): Promise { - const found = await comboBoxElement.findAllByCssSelector( - '[data-test-subj="comboBoxClearButton"]', + const found = await comboBoxElement.findAllByTestSubject( + 'comboBoxClearButton', WAIT_FOR_EXISTS_TIME ); return found.length > 0; @@ -264,9 +264,7 @@ export function ComboBoxProvider({ getService, getPageObjects }: FtrProviderCont public async openOptionsList(comboBoxElement: WebElementWrapper): Promise { const isOptionsListOpen = await testSubjects.exists('~comboBoxOptionsList'); if (!isOptionsListOpen) { - const toggleBtn = await comboBoxElement.findByCssSelector( - '[data-test-subj="comboBoxToggleListButton"]' - ); + const toggleBtn = await comboBoxElement.findByTestSubject('comboBoxToggleListButton'); await toggleBtn.click(); } } diff --git a/test/functional/services/doc_table.ts b/test/functional/services/doc_table.ts index cb3daf20c641a..69650f123d99d 100644 --- a/test/functional/services/doc_table.ts +++ b/test/functional/services/doc_table.ts @@ -48,12 +48,12 @@ export function DocTableProvider({ getService, getPageObjects }: FtrProviderCont public async getBodyRows(): Promise { const table = await this.getTable(); - return await table.findAllByCssSelector('[data-test-subj~="docTableRow"]'); + return await table.findAllByTestSubject('~docTableRow'); } public async getAnchorRow(): Promise { const table = await this.getTable(); - return await table.findByCssSelector('[data-test-subj~="docTableAnchorRow"]'); + return await table.findByTestSubject('~docTableAnchorRow'); } public async getRow(options: SelectOptions): Promise { @@ -73,7 +73,7 @@ export function DocTableProvider({ getService, getPageObjects }: FtrProviderCont options: SelectOptions = { isAnchorRow: false, rowIndex: 0 } ): Promise { const row = await this.getRow(options); - const toggle = await row.findByCssSelector('[data-test-subj~="docTableExpandToggleColumn"]'); + const toggle = await row.findByTestSubject('~docTableExpandToggleColumn'); await toggle.click(); } @@ -90,7 +90,7 @@ export function DocTableProvider({ getService, getPageObjects }: FtrProviderCont const detailsRow = options.isAnchorRow ? await this.getAnchorDetailsRow() : (await this.getDetailsRows())[options.rowIndex]; - return await detailsRow.findAllByCssSelector('[data-test-subj~="docTableRowAction"]'); + return await detailsRow.findAllByTestSubject('~docTableRowAction'); } public async getFields(options: { isAnchorRow: boolean } = { isAnchorRow: false }) { @@ -122,15 +122,13 @@ export function DocTableProvider({ getService, getPageObjects }: FtrProviderCont detailsRow: WebElementWrapper, fieldName: WebElementWrapper ): Promise { - return await detailsRow.findByCssSelector(`[data-test-subj~="tableDocViewRow-${fieldName}"]`); + return await detailsRow.findByTestSubject(`~tableDocViewRow-${fieldName}`); } public async getAddInclusiveFilterButton( tableDocViewRow: WebElementWrapper ): Promise { - return await tableDocViewRow.findByCssSelector( - `[data-test-subj~="addInclusiveFilterButton"]` - ); + return await tableDocViewRow.findByTestSubject(`~addInclusiveFilterButton`); } public async addInclusiveFilter( @@ -146,7 +144,7 @@ export function DocTableProvider({ getService, getPageObjects }: FtrProviderCont public async getAddExistsFilterButton( tableDocViewRow: WebElementWrapper ): Promise { - return await tableDocViewRow.findByCssSelector(`[data-test-subj~="addExistsFilterButton"]`); + return await tableDocViewRow.findByTestSubject(`~addExistsFilterButton`); } public async addExistsFilter( @@ -171,7 +169,7 @@ export function DocTableProvider({ getService, getPageObjects }: FtrProviderCont const detailsRow = await row.findByXpath( './following-sibling::*[@data-test-subj="docTableDetailsRow"]' ); - return detailsRow.findByCssSelector('[data-test-subj~="docViewer"]'); + return detailsRow.findByTestSubject('~docViewer'); }); } } diff --git a/test/functional/services/lib/web_element_wrapper/web_element_wrapper.ts b/test/functional/services/lib/web_element_wrapper/web_element_wrapper.ts index fe781c2ac02b6..157918df874c8 100644 --- a/test/functional/services/lib/web_element_wrapper/web_element_wrapper.ts +++ b/test/functional/services/lib/web_element_wrapper/web_element_wrapper.ts @@ -537,6 +537,40 @@ export class WebElementWrapper { }); } + /** + * Gets the first element inside this element matching the given data-test-subj selector. + * + * @param {string} selector + * @return {Promise} + */ + public async findByTestSubject(selector: string) { + return await this.retryCall(async function find(wrapper) { + return wrapper._wrap( + await wrapper._webElement.findElement(wrapper.By.css(testSubjSelector(selector))), + wrapper.By.css(selector) + ); + }); + } + + /** + * Gets all elements inside this element matching the given data-test-subj selector. + * + * @param {string} selector + * @param {number} timeout + * @return {Promise} + */ + public async findAllByTestSubject(selector: string, timeout?: number) { + return await this.retryCall(async function findAll(wrapper) { + return wrapper._wrapAll( + await wrapper._findWithCustomTimeout( + async () => + await wrapper._webElement.findElements(wrapper.By.css(testSubjSelector(selector))), + timeout + ) + ); + }); + } + /** * Gets the first element inside this element matching the given CSS class name. * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_WebElement.html#findElement diff --git a/x-pack/test/functional/page_objects/gis_page.js b/x-pack/test/functional/page_objects/gis_page.js index 8d0c649d75dd6..1d0e231d7dc54 100644 --- a/x-pack/test/functional/page_objects/gis_page.js +++ b/x-pack/test/functional/page_objects/gis_page.js @@ -451,7 +451,7 @@ export function GisPageProvider({ getService, getPageObjects }) { async getCodeBlockParsedJson(dataTestSubjName) { log.debug(`Get parsed code block for ${dataTestSubjName}`); - const indexRespCodeBlock = await find.byCssSelector(`[data-test-subj="${dataTestSubjName}"]`); + const indexRespCodeBlock = await testSubjects.find(`${dataTestSubjName}`); const indexRespJson = await indexRespCodeBlock.getAttribute('innerText'); return JSON.parse(indexRespJson); } diff --git a/x-pack/test/functional/page_objects/rollup_page.js b/x-pack/test/functional/page_objects/rollup_page.js index 1514693defecb..b6bc60df6f7cd 100644 --- a/x-pack/test/functional/page_objects/rollup_page.js +++ b/x-pack/test/functional/page_objects/rollup_page.js @@ -111,28 +111,18 @@ export function RollupPageProvider({ getService, getPageObjects }) { async getJobList() { const jobs = await testSubjects.findAll('jobTableRow'); return mapAsync(jobs, async job => { - const jobNameElement = await job.findByCssSelector('[data-test-subj="jobTableCell-id"]'); - const jobStatusElement = await job.findByCssSelector( - '[data-test-subj="jobTableCell-status"]' + const jobNameElement = await job.findByTestSubject('jobTableCell-id'); + const jobStatusElement = await job.findByTestSubject('jobTableCell-status'); + const jobIndexPatternElement = await job.findByTestSubject('jobTableCell-indexPattern'); + const jobRollUpIndexPatternElement = await job.findByTestSubject( + 'jobTableCell-rollupIndex' ); - const jobIndexPatternElement = await job.findByCssSelector( - '[data-test-subj="jobTableCell-indexPattern"]' - ); - const jobRollUpIndexPatternElement = await job.findByCssSelector( - '[data-test-subj="jobTableCell-rollupIndex"]' - ); - const jobDelayElement = await job.findByCssSelector( - '[data-test-subj="jobTableCell-rollupDelay"]' - ); - const jobIntervalElement = await job.findByCssSelector( - '[data-test-subj="jobTableCell-dateHistogramInterval"]' - ); - const jobGroupElement = await job.findByCssSelector( - '[data-test-subj="jobTableCell-groups"]' - ); - const jobMetricsElement = await job.findByCssSelector( - '[data-test-subj="jobTableCell-metrics"]' + const jobDelayElement = await job.findByTestSubject('jobTableCell-rollupDelay'); + const jobIntervalElement = await job.findByTestSubject( + 'jobTableCell-dateHistogramInterval' ); + const jobGroupElement = await job.findByTestSubject('jobTableCell-groups'); + const jobMetricsElement = await job.findByTestSubject('jobTableCell-metrics'); return { jobName: await jobNameElement.getVisibleText(), diff --git a/x-pack/test/functional/page_objects/security_page.js b/x-pack/test/functional/page_objects/security_page.js index 4b097b916573d..b399327012a77 100644 --- a/x-pack/test/functional/page_objects/security_page.js +++ b/x-pack/test/functional/page_objects/security_page.js @@ -229,13 +229,12 @@ export function SecurityPageProvider({ getService, getPageObjects }) { async getElasticsearchUsers() { const users = await testSubjects.findAll('userRow'); return mapAsync(users, async user => { - const fullnameElement = await user.findByCssSelector('[data-test-subj="userRowFullName"]'); - const usernameElement = await user.findByCssSelector('[data-test-subj="userRowUserName"]'); - const emailElement = await user.findByCssSelector('[data-test-subj="userRowEmail"]'); - const rolesElement = await user.findByCssSelector('[data-test-subj="userRowRoles"]'); - // findAllByCssSelector is substantially faster than `find.descendantExistsByCssSelector for negative cases - const isUserReserved = - (await user.findAllByCssSelector('span[data-test-subj="userReserved"]', 1)).length > 0; + const fullnameElement = await user.findByTestSubject('userRowFullName'); + const usernameElement = await user.findByTestSubject('userRowUserName'); + const emailElement = await user.findByTestSubject('userRowEmail'); + const rolesElement = await user.findByTestSubject('userRowRoles'); + // findAll is substantially faster than `find.descendantExistsByCssSelector for negative cases + const isUserReserved = (await user.findAllByTestSubject('userReserved', 1)).length > 0; return { username: await usernameElement.getVisibleText(), @@ -251,15 +250,11 @@ export function SecurityPageProvider({ getService, getPageObjects }) { const users = await testSubjects.findAll('roleRow'); return mapAsync(users, async role => { const [rolename, reserved, deprecated] = await Promise.all([ - role.findByCssSelector('[data-test-subj="roleRowName"]').then(el => el.getVisibleText()), - // findAllByCssSelector is substantially faster than `find.descendantExistsByCssSelector for negative cases - role - .findAllByCssSelector('span[data-test-subj="roleReserved"]', 1) - .then(el => el.length > 0), - // findAllByCssSelector is substantially faster than `find.descendantExistsByCssSelector for negative cases - role - .findAllByCssSelector('span[data-test-subj="roleDeprecated"]', 1) - .then(el => el.length > 0), + role.findByTestSubject('roleRowName').then(el => el.getVisibleText()), + // findAll is substantially faster than `find.descendantExistsByCssSelector for negative cases + role.findAllByTestSubject('roleReserved', 1).then(el => el.length > 0), + // findAll is substantially faster than `find.descendantExistsByCssSelector for negative cases + role.findAllByTestSubject('roleDeprecated', 1).then(el => el.length > 0), ]); return { diff --git a/x-pack/test/functional/page_objects/snapshot_restore_page.ts b/x-pack/test/functional/page_objects/snapshot_restore_page.ts index 1c8ba9f633111..841345e3727f1 100644 --- a/x-pack/test/functional/page_objects/snapshot_restore_page.ts +++ b/x-pack/test/functional/page_objects/snapshot_restore_page.ts @@ -28,21 +28,15 @@ export function SnapshotRestorePageProvider({ getService }: FtrProviderContext) }, async getRepoList() { const table = await testSubjects.find('repositoryTable'); - const rows = await table.findAllByCssSelector('[data-test-subj="row"]'); + const rows = await table.findAllByTestSubject('row'); return await Promise.all( rows.map(async row => { return { - repoName: await ( - await row.findByCssSelector('[data-test-subj="Name_cell"]') - ).getVisibleText(), - repoLink: await ( - await row.findByCssSelector('[data-test-subj="Name_cell"]') - ).findByCssSelector('a'), - repoType: await ( - await row.findByCssSelector('[data-test-subj="Type_cell"]') - ).getVisibleText(), - repoEdit: await row.findByCssSelector('[data-test-subj="editRepositoryButton"]'), - repoDelete: await row.findByCssSelector('[data-test-subj="deleteRepositoryButton"]'), + repoName: await (await row.findByTestSubject('Name_cell')).getVisibleText(), + repoLink: await (await row.findByTestSubject('Name_cell')).findByCssSelector('a'), + repoType: await (await row.findByTestSubject('Type_cell')).getVisibleText(), + repoEdit: await row.findByTestSubject('editRepositoryButton'), + repoDelete: await row.findByTestSubject('deleteRepositoryButton'), }; }) ); diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts index 79448fa535370..266e128fd6bee 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts @@ -84,7 +84,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await testSubjects.click('slackAddVariableButton'); const variableMenuButton = await testSubjects.find('variableMenuButton-0'); await variableMenuButton.click(); - await find.clickByCssSelector('[data-test-subj="saveAlertButton"]'); + await testSubjects.click('saveAlertButton'); const toastTitle = await pageObjects.common.closeToast(); expect(toastTitle).to.eql(`Saved '${alertName}'`); await pageObjects.triggersActionsUI.searchAlerts(alertName); @@ -333,9 +333,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await testSubjects.click('collapsedItemActions'); await testSubjects.click('deleteAlert'); - const emptyPrompt = await find.byCssSelector( - '[data-test-subj="createFirstAlertEmptyPrompt"]' - ); + const emptyPrompt = await testSubjects.find('createFirstAlertEmptyPrompt'); expect(await emptyPrompt.elementHasClass('euiEmptyPrompt')).to.be(true); }); @@ -446,9 +444,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await testSubjects.click('deleteAll'); - const emptyPrompt = await find.byCssSelector( - '[data-test-subj="createFirstAlertEmptyPrompt"]' - ); + const emptyPrompt = await testSubjects.find('createFirstAlertEmptyPrompt'); expect(await emptyPrompt.elementHasClass('euiEmptyPrompt')).to.be(true); }); }); diff --git a/x-pack/test/functional_with_es_ssl/page_objects/triggers_actions_ui_page.ts b/x-pack/test/functional_with_es_ssl/page_objects/triggers_actions_ui_page.ts index 91c7fe1f97d12..8d90d3c84b181 100644 --- a/x-pack/test/functional_with_es_ssl/page_objects/triggers_actions_ui_page.ts +++ b/x-pack/test/functional_with_es_ssl/page_objects/triggers_actions_ui_page.ts @@ -19,14 +19,14 @@ export function TriggersActionsPageProvider({ getService }: FtrProviderContext) return await testSubjects.getVisibleText('appTitle'); }, async clickCreateFirstConnectorButton() { - const createBtn = await find.byCssSelector('[data-test-subj="createFirstActionButton"]'); + const createBtn = await testSubjects.find('createFirstActionButton'); const createBtnIsVisible = await createBtn.isDisplayed(); if (createBtnIsVisible) { await createBtn.click(); } }, async clickCreateConnectorButton() { - const createBtn = await find.byCssSelector('[data-test-subj="createActionButton"]'); + const createBtn = await testSubjects.find('createActionButton'); const createBtnIsVisible = await createBtn.isDisplayed(); if (createBtnIsVisible) { await createBtn.click(); From 64e09af107b11ecf154a38617fc0f12ed101bd9b Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Fri, 20 Mar 2020 07:18:54 -0700 Subject: [PATCH 4/9] Implemented ability to clear and properly validate alert interval (#60571) * Implemented ability to clear and properly validate alert interval * Fixed due to comments * Fixed additional request for the last field * Fixed failing test --- .../alerting/common/parse_duration.test.ts | 32 ++++++++++++++++- .../plugins/alerting/common/parse_duration.ts | 9 +++++ .../threshold/expression.tsx | 4 +-- .../sections/alert_form/alert_form.test.tsx | 4 +-- .../sections/alert_form/alert_form.tsx | 34 +++++++++++++------ .../expression_items/for_the_last.test.tsx | 2 +- .../common/expression_items/for_the_last.tsx | 14 ++++---- .../apps/triggers_actions_ui/alerts.ts | 1 - 8 files changed, 75 insertions(+), 25 deletions(-) diff --git a/x-pack/plugins/alerting/common/parse_duration.test.ts b/x-pack/plugins/alerting/common/parse_duration.test.ts index ccdddd8ecf5f4..41d3ab5868c9e 100644 --- a/x-pack/plugins/alerting/common/parse_duration.test.ts +++ b/x-pack/plugins/alerting/common/parse_duration.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { parseDuration } from './parse_duration'; +import { parseDuration, getDurationNumberInItsUnit, getDurationUnitValue } from './parse_duration'; test('parses seconds', () => { const result = parseDuration('10s'); @@ -52,3 +52,33 @@ test('throws error when 0 based', () => { `"Invalid duration \\"0d\\". Durations must be of the form {number}x. Example: 5s, 5m, 5h or 5d\\""` ); }); + +test('getDurationNumberInItsUnit days', () => { + const result = getDurationNumberInItsUnit('10d'); + expect(result).toEqual(10); +}); + +test('getDurationNumberInItsUnit minutes', () => { + const result = getDurationNumberInItsUnit('1m'); + expect(result).toEqual(1); +}); + +test('getDurationNumberInItsUnit seconds', () => { + const result = getDurationNumberInItsUnit('123s'); + expect(result).toEqual(123); +}); + +test('getDurationUnitValue minutes', () => { + const result = getDurationUnitValue('1m'); + expect(result).toEqual('m'); +}); + +test('getDurationUnitValue days', () => { + const result = getDurationUnitValue('23d'); + expect(result).toEqual('d'); +}); + +test('getDurationUnitValue hours', () => { + const result = getDurationUnitValue('100h'); + expect(result).toEqual('h'); +}); diff --git a/x-pack/plugins/alerting/common/parse_duration.ts b/x-pack/plugins/alerting/common/parse_duration.ts index 4e35a4c4cb0cf..c271035f012e5 100644 --- a/x-pack/plugins/alerting/common/parse_duration.ts +++ b/x-pack/plugins/alerting/common/parse_duration.ts @@ -25,6 +25,15 @@ export function parseDuration(duration: string): number { ); } +export function getDurationNumberInItsUnit(duration: string): number { + return parseInt(duration.replace(/[^0-9.]/g, ''), 0); +} + +export function getDurationUnitValue(duration: string): string { + const durationNumber = getDurationNumberInItsUnit(duration); + return duration.replace(durationNumber.toString(), ''); +} + export function validateDurationSchema(duration: string) { if (duration.match(SECONDS_REGEX)) { return; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/expression.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/expression.tsx index 728418bf3c336..fa26e8b11bfec 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/expression.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/expression.tsx @@ -399,8 +399,8 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent setAlertParams('timeWindowSize', selectedWindowSize) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx index b31be7ecb9a79..b87aaacb3ec0e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx @@ -86,7 +86,7 @@ describe('alert_form', () => { uiSettings: deps!.uiSettings, }} > - {}} errors={{ name: [] }} /> + {}} errors={{ name: [], interval: [] }} /> ); @@ -165,7 +165,7 @@ describe('alert_form', () => { uiSettings: deps!.uiSettings, }} > - {}} errors={{ name: [] }} /> + {}} errors={{ name: [], interval: [] }} /> ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx index 1fa620c5394a1..8382cbe825da3 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx @@ -24,6 +24,10 @@ import { EuiButtonIcon, EuiHorizontalRule, } from '@elastic/eui'; +import { + getDurationNumberInItsUnit, + getDurationUnitValue, +} from '../../../../../alerting/common/parse_duration'; import { loadAlertTypes } from '../../lib/alert_api'; import { actionVariablesFromAlertType } from '../../lib/action_variables'; import { AlertReducerAction } from './alert_reducer'; @@ -48,7 +52,7 @@ export function validateBaseProperties(alertObject: Alert) { }) ); } - if (!alertObject.schedule.interval) { + if (alertObject.schedule.interval.length < 2) { errors.interval.push( i18n.translate('xpack.triggersActionsUI.sections.alertForm.error.requiredIntervalText', { defaultMessage: 'Check interval is required.', @@ -81,17 +85,17 @@ export const AlertForm = ({ alert, canChangeTrigger = true, dispatch, errors }: ); const [alertTypesIndex, setAlertTypesIndex] = useState(undefined); - const [alertInterval, setAlertInterval] = useState( - alert.schedule.interval ? parseInt(alert.schedule.interval.replace(/^[A-Za-z]+$/, ''), 0) : 1 + const [alertInterval, setAlertInterval] = useState( + alert.schedule.interval ? getDurationNumberInItsUnit(alert.schedule.interval) : undefined ); const [alertIntervalUnit, setAlertIntervalUnit] = useState( - alert.schedule.interval ? alert.schedule.interval.replace(alertInterval.toString(), '') : 'm' + alert.schedule.interval ? getDurationUnitValue(alert.schedule.interval) : 'm' ); const [alertThrottle, setAlertThrottle] = useState( - alert.throttle ? parseInt(alert.throttle.replace(/^[A-Za-z]+$/, ''), 0) : null + alert.throttle ? getDurationNumberInItsUnit(alert.throttle) : null ); const [alertThrottleUnit, setAlertThrottleUnit] = useState( - alert.throttle ? alert.throttle.replace((alertThrottle ?? '').toString(), '') : 'm' + alert.throttle ? getDurationUnitValue(alert.throttle) : 'm' ); const [defaultActionGroupId, setDefaultActionGroupId] = useState(undefined); @@ -344,19 +348,27 @@ export const AlertForm = ({ alert, canChangeTrigger = true, dispatch, errors }: - + 0} + error={errors.interval} + > 0} compressed - value={alertInterval} + value={alertInterval || ''} name="interval" data-test-subj="intervalInput" onChange={e => { - const interval = e.target.value !== '' ? parseInt(e.target.value, 10) : null; - setAlertInterval(interval ?? 1); + const interval = + e.target.value !== '' ? parseInt(e.target.value, 10) : undefined; + setAlertInterval(interval); setScheduleProperty('interval', `${e.target.value}${alertIntervalUnit}`); }} /> @@ -366,7 +378,7 @@ export const AlertForm = ({ alert, canChangeTrigger = true, dispatch, errors }: fullWidth compressed value={alertIntervalUnit} - options={getTimeOptions(alertInterval)} + options={getTimeOptions(alertInterval ?? 1)} onChange={e => { setAlertIntervalUnit(e.target.value); setScheduleProperty('interval', `${alertInterval}${e.target.value}`); diff --git a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/for_the_last.test.tsx b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/for_the_last.test.tsx index 6ae3056001c8f..e66bb1e7b4b9a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/for_the_last.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/for_the_last.test.tsx @@ -36,7 +36,7 @@ describe('for the last expression', () => { /> ); wrapper.simulate('click'); - expect(wrapper.find('[value=1]').length > 0).toBeTruthy(); + expect(wrapper.find('[value=""]').length > 0).toBeTruthy(); expect(wrapper.find('[value="s"]').length > 0).toBeTruthy(); expect( wrapper.contains( diff --git a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/for_the_last.tsx b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/for_the_last.tsx index 844551de3171d..673391dd9cbad 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/for_the_last.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/for_the_last.tsx @@ -25,7 +25,7 @@ interface ForLastExpressionProps { timeWindowSize?: number; timeWindowUnit?: string; errors: { [key: string]: string[] }; - onChangeWindowSize: (selectedWindowSize: number | '') => void; + onChangeWindowSize: (selectedWindowSize: number | undefined) => void; onChangeWindowUnit: (selectedWindowUnit: string) => void; popupPosition?: | 'upCenter' @@ -43,7 +43,7 @@ interface ForLastExpressionProps { } export const ForLastExpression = ({ - timeWindowSize = 1, + timeWindowSize, timeWindowUnit = 's', errors, onChangeWindowSize, @@ -64,7 +64,7 @@ export const ForLastExpression = ({ )} value={`${timeWindowSize} ${getTimeUnitLabel( timeWindowUnit as TIME_UNITS, - timeWindowSize.toString() + (timeWindowSize ?? '').toString() )}`} isActive={alertDurationPopoverOpen} onClick={() => { @@ -97,11 +97,11 @@ export const ForLastExpression = ({ 0 && timeWindowSize !== undefined} - min={1} - value={timeWindowSize} + min={0} + value={timeWindowSize || ''} onChange={e => { const { value } = e.target; - const timeWindowSizeVal = value !== '' ? parseInt(value, 10) : value; + const timeWindowSizeVal = value !== '' ? parseInt(value, 10) : undefined; onChangeWindowSize(timeWindowSizeVal); }} /> @@ -114,7 +114,7 @@ export const ForLastExpression = ({ onChange={e => { onChangeWindowUnit(e.target.value); }} - options={getTimeOptions(timeWindowSize)} + options={getTimeOptions(timeWindowSize ?? 1)} /> diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts index 266e128fd6bee..eb4b7d3b93a49 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts @@ -63,7 +63,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await fieldOptions[1].click(); // need this two out of popup clicks to close them await nameInput.click(); - await testSubjects.click('intervalInput'); await testSubjects.click('.slack-ActionTypeSelectOption'); await testSubjects.click('createActionConnectorButton'); From 851b8a82a5b95bf8b7e8abb436f7491cb9789e3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20C=C3=B4t=C3=A9?= Date: Fri, 20 Mar 2020 10:49:37 -0400 Subject: [PATCH 5/9] License checks for actions plugin (#59070) * Define minimum license required for each action type (#58668) * Add minimum required license * Require at least gold license as a minimum license required on third party action types * Use strings for license references * Ensure license type is valid * Fix some tests * Add servicenow to gold * Add tests * Set license requirements on other built in action types * Use jest.Mocked instead * Change servicenow to platinum Co-authored-by: Elastic Machine * Make actions config mock and license state mock use factory pattern and jest mocks (#59370) * Add license checks to action HTTP APIs (#59153) * Initial work * Handle errors in update action API * Add unit tests for APIs * Make action executor throw when action type isn't enabled * Add test suite for basic license * Fix ESLint errors * Fix failing tests * Attempt 1 to fix CI * ESLint fixes * Create sendResponse function on ActionTypeDisabledError * Make disabled action types by config return 403 * Remove switch case * Fix ESLint * Add license checks within alerting / actions framework (#59699) * Initial work * Handle errors in update action API * Add unit tests for APIs * Verify action type before scheduling action task * Make actions plugin.execute throw error if action type is disabled * Bug fixes * Make action executor throw when action type isn't enabled * Add test suite for basic license * Fix ESLint errors * Stop action task from re-running when license check fails * Fix failing tests * Attempt 1 to fix CI * ESLint fixes * Create sendResponse function on ActionTypeDisabledError * Make disabled action types by config return 403 * Remove switch case * Fix ESLint * Fix confusing assertion * Add comment explaining double mock * Log warning when alert action isn't scheduled * Disable action types in UI when license doesn't support it (#59819) * Initial work * Handle errors in update action API * Add unit tests for APIs * Verify action type before scheduling action task * Make actions plugin.execute throw error if action type is disabled * Bug fixes * Make action executor throw when action type isn't enabled * Add test suite for basic license * Fix ESLint errors * Stop action task from re-running when license check fails * Fix failing tests * Attempt 1 to fix CI * ESLint fixes * Return enabledInConfig and enabledInLicense from actions get types API * Disable cards that have invalid license in create connector flyout * Create sendResponse function on ActionTypeDisabledError * Make disabled action types by config return 403 * Remove switch case * Fix ESLint * Disable when creating alert action * Return minimumLicenseRequired in /types API * Disable row in connectors when action type is disabled * Fix failing jest test * Some refactoring * Card in edit alert flyout * Sort action types by name * Add tooltips to create connector action type selector * Add tooltips to alert flyout action type selector * Add get more actions link in alert flyout * Add callout when creating a connector * Typos * remove float right and use flexgroup * replace pixels with eui variables * turn on sass lint for triggers_actions_ui dir * trying to add padding around cards * Add callout in edit alert screen when some actions are disabled * improve card selection for Add Connector flyout * Fix cards for create connector * Add tests * ESLint issue * Cleanup * Cleanup pt2 * Fix type check errors * moving to 3-columns cards for connector selection * Change re-enable to enable terminology * Revert "Change re-enable to enable terminology" This reverts commit b497dfd6b6bc88db862ad97826e8d03b094c8ed0. * Add re-enable comment * Remove unecessary fragment * Add type to actionTypeNodes * Fix EuiLink to not have opacity of 0.7 when not hovered * design cleanup in progress * updating classNames * using EuiIconTip * Remove label on icon tip * Fix failing jest test Co-authored-by: Andrea Del Rio * Add index to .index action type test * PR feedback * Add isErrorThatHandlesItsOwnResponse Co-authored-by: Elastic Machine Co-authored-by: Andrea Del Rio --- .sass-lint.yml | 1 + .../case/components/configure_cases/index.tsx | 6 +- x-pack/plugins/actions/common/types.ts | 5 + .../server/action_type_registry.mock.ts | 1 + .../server/action_type_registry.test.ts | 133 +++++++++- .../actions/server/action_type_registry.ts | 25 +- .../actions/server/actions_client.test.ts | 108 +++++++- .../plugins/actions/server/actions_client.ts | 9 +- .../actions/server/actions_config.mock.ts | 21 +- .../plugins/actions/server/actions_config.ts | 3 +- .../server/builtin_action_types/email.test.ts | 11 +- .../server/builtin_action_types/email.ts | 1 + .../server/builtin_action_types/es_index.ts | 1 + .../server/builtin_action_types/index.test.ts | 8 +- .../builtin_action_types/pagerduty.test.ts | 6 +- .../server/builtin_action_types/pagerduty.ts | 1 + .../server/builtin_action_types/server_log.ts | 1 + .../servicenow/index.test.ts | 6 +- .../builtin_action_types/servicenow/index.ts | 1 + .../server/builtin_action_types/slack.test.ts | 10 +- .../server/builtin_action_types/slack.ts | 1 + .../builtin_action_types/webhook.test.ts | 6 +- .../server/builtin_action_types/webhook.ts | 1 + .../server/create_execute_function.test.ts | 37 +++ .../actions/server/create_execute_function.ts | 7 +- .../server/lib/action_executor.test.ts | 30 +-- .../actions/server/lib/action_executor.ts | 6 +- .../server/lib/errors/action_type_disabled.ts | 27 ++ .../actions/server/lib/errors/index.ts | 15 ++ .../actions/server/lib/errors/types.ts | 11 + x-pack/plugins/actions/server/lib/index.ts | 7 + .../actions/server/lib/license_state.mock.ts | 43 ++-- .../actions/server/lib/license_state.test.ts | 137 ++++++++++- .../actions/server/lib/license_state.ts | 71 ++++++ .../server/lib/task_runner_factory.test.ts | 31 +++ .../actions/server/lib/task_runner_factory.ts | 29 ++- .../server/lib/validate_with_schema.test.ts | 19 +- ...nse_api_access.ts => verify_api_access.ts} | 4 +- x-pack/plugins/actions/server/mocks.ts | 1 + x-pack/plugins/actions/server/plugin.test.ts | 56 ++++- x-pack/plugins/actions/server/plugin.ts | 31 ++- .../actions/server/routes/create.test.ts | 31 ++- .../plugins/actions/server/routes/create.ts | 20 +- .../actions/server/routes/delete.test.ts | 12 +- .../plugins/actions/server/routes/delete.ts | 5 +- .../actions/server/routes/execute.test.ts | 44 +++- .../plugins/actions/server/routes/execute.ts | 32 ++- .../actions/server/routes/find.test.ts | 12 +- x-pack/plugins/actions/server/routes/find.ts | 5 +- .../plugins/actions/server/routes/get.test.ts | 12 +- x-pack/plugins/actions/server/routes/get.ts | 5 +- .../server/routes/list_action_types.test.ts | 12 +- .../server/routes/list_action_types.ts | 5 +- .../actions/server/routes/update.test.ts | 34 ++- .../plugins/actions/server/routes/update.ts | 25 +- x-pack/plugins/actions/server/types.ts | 2 + x-pack/plugins/alerting/server/plugin.ts | 2 +- .../create_execution_handler.test.ts | 65 ++++- .../task_runner/create_execution_handler.ts | 22 +- .../server/task_runner/task_runner.test.ts | 16 +- .../server/task_runner/task_runner.ts | 2 +- .../task_runner/task_runner_factory.test.ts | 3 +- .../server/task_runner/task_runner_factory.ts | 2 +- .../lib/action_connector_api.test.ts | 3 + .../lib/action_type_compare.test.ts | 74 ++++++ .../application/lib/action_type_compare.ts | 17 ++ .../lib/check_action_type_enabled.scss | 9 + .../lib/check_action_type_enabled.test.tsx | 88 +++++++ .../lib/check_action_type_enabled.tsx | 93 +++++++ .../action_form.test.tsx | 96 +++++++- .../action_connector_form/action_form.tsx | 232 ++++++++++++------ .../action_type_menu.test.tsx | 106 ++++++++ .../action_type_menu.tsx | 70 ++++-- .../connector_add_flyout.test.tsx | 3 + .../connector_add_flyout.tsx | 38 ++- .../connector_add_modal.test.tsx | 7 +- .../components/actions_connectors_list.scss | 12 + .../actions_connectors_list.test.tsx | 113 +++++++++ .../components/actions_connectors_list.tsx | 42 +++- .../components/alert_details.test.tsx | 9 + .../sections/alert_form/alert_edit.tsx | 27 +- .../sections/alert_form/alert_form.tsx | 10 +- .../alerts_list/components/alerts_list.tsx | 40 ++- .../public/common/constants/index.ts | 2 + .../triggers_actions_ui/public/index.ts | 2 +- x-pack/scripts/functional_tests.js | 1 + .../alerting_api_integration/basic/config.ts | 14 ++ .../actions/builtin_action_types/email.ts | 38 +++ .../actions/builtin_action_types/es_index.ts | 30 +++ .../actions/builtin_action_types/pagerduty.ts | 36 +++ .../builtin_action_types/server_log.ts | 28 +++ .../builtin_action_types/servicenow.ts | 83 +++++++ .../actions/builtin_action_types/slack.ts | 48 ++++ .../actions/builtin_action_types/webhook.ts | 51 ++++ .../basic/tests/actions/index.ts | 20 ++ .../basic/tests/index.ts | 19 ++ .../alerting_api_integration/common/config.ts | 3 +- .../common/fixtures/plugins/actions/index.ts | 1 + .../common/fixtures/plugins/alerts/index.ts | 5 + .../tests/actions/create.ts | 6 +- .../spaces_only/config.ts | 2 +- .../tests/actions/type_not_enabled.ts | 25 +- 102 files changed, 2402 insertions(+), 397 deletions(-) create mode 100644 x-pack/plugins/actions/server/lib/errors/action_type_disabled.ts create mode 100644 x-pack/plugins/actions/server/lib/errors/index.ts create mode 100644 x-pack/plugins/actions/server/lib/errors/types.ts rename x-pack/plugins/actions/server/lib/{license_api_access.ts => verify_api_access.ts} (81%) create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/action_type_compare.test.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/action_type_compare.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/check_action_type_enabled.scss create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/check_action_type_enabled.test.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/check_action_type_enabled.tsx create mode 100644 x-pack/test/alerting_api_integration/basic/config.ts create mode 100644 x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/email.ts create mode 100644 x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/es_index.ts create mode 100644 x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/pagerduty.ts create mode 100644 x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/server_log.ts create mode 100644 x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/servicenow.ts create mode 100644 x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/slack.ts create mode 100644 x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/webhook.ts create mode 100644 x-pack/test/alerting_api_integration/basic/tests/actions/index.ts create mode 100644 x-pack/test/alerting_api_integration/basic/tests/index.ts diff --git a/.sass-lint.yml b/.sass-lint.yml index 9c64c1e5eea56..dd7bc0576692b 100644 --- a/.sass-lint.yml +++ b/.sass-lint.yml @@ -7,6 +7,7 @@ files: - 'x-pack/legacy/plugins/rollup/**/*.s+(a|c)ss' - 'x-pack/legacy/plugins/security/**/*.s+(a|c)ss' - 'x-pack/legacy/plugins/canvas/**/*.s+(a|c)ss' + - 'x-pack/plugins/triggers_actions_ui/**/*.s+(a|c)ss' ignore: - 'x-pack/legacy/plugins/canvas/shareable_runtime/**/*.s+(a|c)ss' - 'x-pack/legacy/plugins/lens/**/*.s+(a|c)ss' diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/index.tsx index cbc3be6d144a2..c8ef6e32595d0 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/index.tsx @@ -21,6 +21,7 @@ import { useConnectors } from '../../../../containers/case/configure/use_connect import { useCaseConfigure } from '../../../../containers/case/configure/use_configure'; import { ActionsConnectorsContextProvider, + ActionType, ConnectorAddFlyout, ConnectorEditFlyout, } from '../../../../../../../../plugins/triggers_actions_ui/public'; @@ -60,11 +61,14 @@ const initialState: State = { mapping: null, }; -const actionTypes = [ +const actionTypes: ActionType[] = [ { id: '.servicenow', name: 'ServiceNow', enabled: true, + enabledInConfig: true, + enabledInLicense: true, + minimumLicenseRequired: 'platinum', }, ]; diff --git a/x-pack/plugins/actions/common/types.ts b/x-pack/plugins/actions/common/types.ts index fbd7404a2f15e..f3042a701211f 100644 --- a/x-pack/plugins/actions/common/types.ts +++ b/x-pack/plugins/actions/common/types.ts @@ -4,10 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ +import { LicenseType } from '../../licensing/common/types'; + export interface ActionType { id: string; name: string; enabled: boolean; + enabledInConfig: boolean; + enabledInLicense: boolean; + minimumLicenseRequired: LicenseType; } export interface ActionResult { diff --git a/x-pack/plugins/actions/server/action_type_registry.mock.ts b/x-pack/plugins/actions/server/action_type_registry.mock.ts index 5589a15932ecf..6a806d1fa531c 100644 --- a/x-pack/plugins/actions/server/action_type_registry.mock.ts +++ b/x-pack/plugins/actions/server/action_type_registry.mock.ts @@ -13,6 +13,7 @@ const createActionTypeRegistryMock = () => { get: jest.fn(), list: jest.fn(), ensureActionTypeEnabled: jest.fn(), + isActionTypeEnabled: jest.fn(), }; return mocked; }; diff --git a/x-pack/plugins/actions/server/action_type_registry.test.ts b/x-pack/plugins/actions/server/action_type_registry.test.ts index bced8841138f2..26bd68adfc4b6 100644 --- a/x-pack/plugins/actions/server/action_type_registry.test.ts +++ b/x-pack/plugins/actions/server/action_type_registry.test.ts @@ -5,21 +5,31 @@ */ import { taskManagerMock } from '../../task_manager/server/task_manager.mock'; -import { ActionTypeRegistry } from './action_type_registry'; -import { ExecutorType } from './types'; -import { ActionExecutor, ExecutorError, TaskRunnerFactory } from './lib'; -import { configUtilsMock } from './actions_config.mock'; +import { ActionTypeRegistry, ActionTypeRegistryOpts } from './action_type_registry'; +import { ActionType, ExecutorType } from './types'; +import { ActionExecutor, ExecutorError, ILicenseState, TaskRunnerFactory } from './lib'; +import { actionsConfigMock } from './actions_config.mock'; +import { licenseStateMock } from './lib/license_state.mock'; +import { ActionsConfigurationUtilities } from './actions_config'; const mockTaskManager = taskManagerMock.setup(); -const actionTypeRegistryParams = { - taskManager: mockTaskManager, - taskRunnerFactory: new TaskRunnerFactory( - new ActionExecutor({ isESOUsingEphemeralEncryptionKey: false }) - ), - actionsConfigUtils: configUtilsMock, -}; +let mockedLicenseState: jest.Mocked; +let mockedActionsConfig: jest.Mocked; +let actionTypeRegistryParams: ActionTypeRegistryOpts; -beforeEach(() => jest.resetAllMocks()); +beforeEach(() => { + jest.resetAllMocks(); + mockedLicenseState = licenseStateMock.create(); + mockedActionsConfig = actionsConfigMock.create(); + actionTypeRegistryParams = { + taskManager: mockTaskManager, + taskRunnerFactory: new TaskRunnerFactory( + new ActionExecutor({ isESOUsingEphemeralEncryptionKey: false }) + ), + actionsConfigUtils: mockedActionsConfig, + licenseState: mockedLicenseState, + }; +}); const executor: ExecutorType = async options => { return { status: 'ok', actionId: options.actionId }; @@ -31,6 +41,7 @@ describe('register()', () => { actionTypeRegistry.register({ id: 'my-action-type', name: 'My action type', + minimumLicenseRequired: 'basic', executor, }); expect(actionTypeRegistry.has('my-action-type')).toEqual(true); @@ -55,12 +66,14 @@ describe('register()', () => { actionTypeRegistry.register({ id: 'my-action-type', name: 'My action type', + minimumLicenseRequired: 'basic', executor, }); expect(() => actionTypeRegistry.register({ id: 'my-action-type', name: 'My action type', + minimumLicenseRequired: 'basic', executor, }) ).toThrowErrorMatchingInlineSnapshot( @@ -73,6 +86,7 @@ describe('register()', () => { actionTypeRegistry.register({ id: 'my-action-type', name: 'My action type', + minimumLicenseRequired: 'basic', executor, }); expect(mockTaskManager.registerTaskDefinitions).toHaveBeenCalledTimes(1); @@ -94,6 +108,7 @@ describe('get()', () => { actionTypeRegistry.register({ id: 'my-action-type', name: 'My action type', + minimumLicenseRequired: 'basic', executor, }); const actionType = actionTypeRegistry.get('my-action-type'); @@ -101,6 +116,7 @@ describe('get()', () => { Object { "executor": [Function], "id": "my-action-type", + "minimumLicenseRequired": "basic", "name": "My action type", } `); @@ -116,10 +132,12 @@ describe('get()', () => { describe('list()', () => { test('returns list of action types', () => { + mockedLicenseState.isLicenseValidForActionType.mockReturnValue({ isValid: true }); const actionTypeRegistry = new ActionTypeRegistry(actionTypeRegistryParams); actionTypeRegistry.register({ id: 'my-action-type', name: 'My action type', + minimumLicenseRequired: 'basic', executor, }); const actionTypes = actionTypeRegistry.list(); @@ -128,8 +146,13 @@ describe('list()', () => { id: 'my-action-type', name: 'My action type', enabled: true, + enabledInConfig: true, + enabledInLicense: true, + minimumLicenseRequired: 'basic', }, ]); + expect(mockedActionsConfig.isActionTypeEnabled).toHaveBeenCalled(); + expect(mockedLicenseState.isLicenseValidForActionType).toHaveBeenCalled(); }); }); @@ -144,8 +167,94 @@ describe('has()', () => { actionTypeRegistry.register({ id: 'my-action-type', name: 'My action type', + minimumLicenseRequired: 'basic', executor, }); expect(actionTypeRegistry.has('my-action-type')); }); }); + +describe('isActionTypeEnabled', () => { + let actionTypeRegistry: ActionTypeRegistry; + const fooActionType: ActionType = { + id: 'foo', + name: 'Foo', + minimumLicenseRequired: 'basic', + executor: async () => {}, + }; + + beforeEach(() => { + actionTypeRegistry = new ActionTypeRegistry(actionTypeRegistryParams); + actionTypeRegistry.register(fooActionType); + }); + + test('should call isActionTypeEnabled of the actions config', async () => { + mockedLicenseState.isLicenseValidForActionType.mockReturnValue({ isValid: true }); + actionTypeRegistry.isActionTypeEnabled('foo'); + expect(mockedActionsConfig.isActionTypeEnabled).toHaveBeenCalledWith('foo'); + }); + + test('should call isLicenseValidForActionType of the license state', async () => { + mockedLicenseState.isLicenseValidForActionType.mockReturnValue({ isValid: true }); + actionTypeRegistry.isActionTypeEnabled('foo'); + expect(mockedLicenseState.isLicenseValidForActionType).toHaveBeenCalledWith(fooActionType); + }); + + test('should return false when isActionTypeEnabled is false and isLicenseValidForActionType is true', async () => { + mockedActionsConfig.isActionTypeEnabled.mockReturnValue(false); + mockedLicenseState.isLicenseValidForActionType.mockReturnValue({ isValid: true }); + expect(actionTypeRegistry.isActionTypeEnabled('foo')).toEqual(false); + }); + + test('should return false when isActionTypeEnabled is true and isLicenseValidForActionType is false', async () => { + mockedActionsConfig.isActionTypeEnabled.mockReturnValue(true); + mockedLicenseState.isLicenseValidForActionType.mockReturnValue({ + isValid: false, + reason: 'invalid', + }); + expect(actionTypeRegistry.isActionTypeEnabled('foo')).toEqual(false); + }); +}); + +describe('ensureActionTypeEnabled', () => { + let actionTypeRegistry: ActionTypeRegistry; + const fooActionType: ActionType = { + id: 'foo', + name: 'Foo', + minimumLicenseRequired: 'basic', + executor: async () => {}, + }; + + beforeEach(() => { + actionTypeRegistry = new ActionTypeRegistry(actionTypeRegistryParams); + actionTypeRegistry.register(fooActionType); + }); + + test('should call ensureActionTypeEnabled of the action config', async () => { + actionTypeRegistry.ensureActionTypeEnabled('foo'); + expect(mockedActionsConfig.ensureActionTypeEnabled).toHaveBeenCalledWith('foo'); + }); + + test('should call ensureLicenseForActionType on the license state', async () => { + actionTypeRegistry.ensureActionTypeEnabled('foo'); + expect(mockedLicenseState.ensureLicenseForActionType).toHaveBeenCalledWith(fooActionType); + }); + + test('should throw when ensureActionTypeEnabled throws', async () => { + mockedActionsConfig.ensureActionTypeEnabled.mockImplementation(() => { + throw new Error('Fail'); + }); + expect(() => + actionTypeRegistry.ensureActionTypeEnabled('foo') + ).toThrowErrorMatchingInlineSnapshot(`"Fail"`); + }); + + test('should throw when ensureLicenseForActionType throws', async () => { + mockedLicenseState.ensureLicenseForActionType.mockImplementation(() => { + throw new Error('Fail'); + }); + expect(() => + actionTypeRegistry.ensureActionTypeEnabled('foo') + ).toThrowErrorMatchingInlineSnapshot(`"Fail"`); + }); +}); diff --git a/x-pack/plugins/actions/server/action_type_registry.ts b/x-pack/plugins/actions/server/action_type_registry.ts index 42e0ee9f523e1..c1d979feacc1d 100644 --- a/x-pack/plugins/actions/server/action_type_registry.ts +++ b/x-pack/plugins/actions/server/action_type_registry.ts @@ -7,15 +7,16 @@ import Boom from 'boom'; import { i18n } from '@kbn/i18n'; import { RunContext, TaskManagerSetupContract } from '../../task_manager/server'; -import { ExecutorError, TaskRunnerFactory } from './lib'; +import { ExecutorError, TaskRunnerFactory, ILicenseState } from './lib'; import { ActionType } from './types'; import { ActionType as CommonActionType } from '../common'; import { ActionsConfigurationUtilities } from './actions_config'; -interface ConstructorOptions { +export interface ActionTypeRegistryOpts { taskManager: TaskManagerSetupContract; taskRunnerFactory: TaskRunnerFactory; actionsConfigUtils: ActionsConfigurationUtilities; + licenseState: ILicenseState; } export class ActionTypeRegistry { @@ -23,11 +24,13 @@ export class ActionTypeRegistry { private readonly actionTypes: Map = new Map(); private readonly taskRunnerFactory: TaskRunnerFactory; private readonly actionsConfigUtils: ActionsConfigurationUtilities; + private readonly licenseState: ILicenseState; - constructor(constructorParams: ConstructorOptions) { + constructor(constructorParams: ActionTypeRegistryOpts) { this.taskManager = constructorParams.taskManager; this.taskRunnerFactory = constructorParams.taskRunnerFactory; this.actionsConfigUtils = constructorParams.actionsConfigUtils; + this.licenseState = constructorParams.licenseState; } /** @@ -42,6 +45,17 @@ export class ActionTypeRegistry { */ public ensureActionTypeEnabled(id: string) { this.actionsConfigUtils.ensureActionTypeEnabled(id); + this.licenseState.ensureLicenseForActionType(this.get(id)); + } + + /** + * Returns true if action type is enabled in the config and a valid license is used. + */ + public isActionTypeEnabled(id: string) { + return ( + this.actionsConfigUtils.isActionTypeEnabled(id) && + this.licenseState.isLicenseValidForActionType(this.get(id)).isValid === true + ); } /** @@ -103,7 +117,10 @@ export class ActionTypeRegistry { return Array.from(this.actionTypes).map(([actionTypeId, actionType]) => ({ id: actionTypeId, name: actionType.name, - enabled: this.actionsConfigUtils.isActionTypeEnabled(actionTypeId), + minimumLicenseRequired: actionType.minimumLicenseRequired, + enabled: this.isActionTypeEnabled(actionTypeId), + enabledInConfig: this.actionsConfigUtils.isActionTypeEnabled(actionTypeId), + enabledInLicense: this.licenseState.isLicenseValidForActionType(actionType).isValid === true, })); } } diff --git a/x-pack/plugins/actions/server/actions_client.test.ts b/x-pack/plugins/actions/server/actions_client.test.ts index cafad6313d2e4..0df07ad58fb9e 100644 --- a/x-pack/plugins/actions/server/actions_client.test.ts +++ b/x-pack/plugins/actions/server/actions_client.test.ts @@ -6,13 +6,14 @@ import { schema } from '@kbn/config-schema'; -import { ActionTypeRegistry } from './action_type_registry'; +import { ActionTypeRegistry, ActionTypeRegistryOpts } from './action_type_registry'; import { ActionsClient } from './actions_client'; import { ExecutorType } from './types'; -import { ActionExecutor, TaskRunnerFactory } from './lib'; +import { ActionExecutor, TaskRunnerFactory, ILicenseState } from './lib'; import { taskManagerMock } from '../../task_manager/server/task_manager.mock'; -import { configUtilsMock } from './actions_config.mock'; +import { actionsConfigMock } from './actions_config.mock'; import { getActionsConfigurationUtilities } from './actions_config'; +import { licenseStateMock } from './lib/license_state.mock'; import { elasticsearchServiceMock, @@ -25,22 +26,25 @@ const scopedClusterClient = elasticsearchServiceMock.createScopedClusterClient() const mockTaskManager = taskManagerMock.setup(); -const actionTypeRegistryParams = { - taskManager: mockTaskManager, - taskRunnerFactory: new TaskRunnerFactory( - new ActionExecutor({ isESOUsingEphemeralEncryptionKey: false }) - ), - actionsConfigUtils: configUtilsMock, -}; - let actionsClient: ActionsClient; +let mockedLicenseState: jest.Mocked; let actionTypeRegistry: ActionTypeRegistry; +let actionTypeRegistryParams: ActionTypeRegistryOpts; const executor: ExecutorType = async options => { return { status: 'ok', actionId: options.actionId }; }; beforeEach(() => { jest.resetAllMocks(); + mockedLicenseState = licenseStateMock.create(); + actionTypeRegistryParams = { + taskManager: mockTaskManager, + taskRunnerFactory: new TaskRunnerFactory( + new ActionExecutor({ isESOUsingEphemeralEncryptionKey: false }) + ), + actionsConfigUtils: actionsConfigMock.create(), + licenseState: mockedLicenseState, + }; actionTypeRegistry = new ActionTypeRegistry(actionTypeRegistryParams); actionsClient = new ActionsClient({ actionTypeRegistry, @@ -65,6 +69,7 @@ describe('create()', () => { actionTypeRegistry.register({ id: 'my-action-type', name: 'My action type', + minimumLicenseRequired: 'basic', executor, }); savedObjectsClient.create.mockResolvedValueOnce(savedObjectCreateResult); @@ -100,6 +105,7 @@ describe('create()', () => { actionTypeRegistry.register({ id: 'my-action-type', name: 'My action type', + minimumLicenseRequired: 'basic', validate: { config: schema.object({ param1: schema.string(), @@ -140,6 +146,7 @@ describe('create()', () => { actionTypeRegistry.register({ id: 'my-action-type', name: 'My action type', + minimumLicenseRequired: 'basic', executor, }); savedObjectsClient.create.mockResolvedValueOnce({ @@ -210,6 +217,7 @@ describe('create()', () => { new ActionExecutor({ isESOUsingEphemeralEncryptionKey: false }) ), actionsConfigUtils: localConfigUtils, + licenseState: licenseStateMock.create(), }; actionTypeRegistry = new ActionTypeRegistry(localActionTypeRegistryParams); @@ -233,6 +241,7 @@ describe('create()', () => { actionTypeRegistry.register({ id: 'my-action-type', name: 'My action type', + minimumLicenseRequired: 'basic', executor, }); savedObjectsClient.create.mockResolvedValueOnce(savedObjectCreateResult); @@ -250,6 +259,39 @@ describe('create()', () => { `"action type \\"my-action-type\\" is not enabled in the Kibana config xpack.actions.enabledActionTypes"` ); }); + + test('throws error when ensureActionTypeEnabled throws', async () => { + const savedObjectCreateResult = { + id: '1', + type: 'type', + attributes: { + name: 'my name', + actionTypeId: 'my-action-type', + config: {}, + }, + references: [], + }; + actionTypeRegistry.register({ + id: 'my-action-type', + name: 'My action type', + minimumLicenseRequired: 'basic', + executor, + }); + mockedLicenseState.ensureLicenseForActionType.mockImplementation(() => { + throw new Error('Fail'); + }); + savedObjectsClient.create.mockResolvedValueOnce(savedObjectCreateResult); + await expect( + actionsClient.create({ + action: { + name: 'my name', + actionTypeId: 'my-action-type', + config: {}, + secrets: {}, + }, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot(`"Fail"`); + }); }); describe('get()', () => { @@ -346,6 +388,7 @@ describe('update()', () => { actionTypeRegistry.register({ id: 'my-action-type', name: 'My action type', + minimumLicenseRequired: 'basic', executor, }); savedObjectsClient.get.mockResolvedValueOnce({ @@ -407,6 +450,7 @@ describe('update()', () => { actionTypeRegistry.register({ id: 'my-action-type', name: 'My action type', + minimumLicenseRequired: 'basic', validate: { config: schema.object({ param1: schema.string(), @@ -440,6 +484,7 @@ describe('update()', () => { actionTypeRegistry.register({ id: 'my-action-type', name: 'My action type', + minimumLicenseRequired: 'basic', executor, }); savedObjectsClient.get.mockResolvedValueOnce({ @@ -505,4 +550,45 @@ describe('update()', () => { ] `); }); + + test('throws an error when ensureActionTypeEnabled throws', async () => { + actionTypeRegistry.register({ + id: 'my-action-type', + name: 'My action type', + minimumLicenseRequired: 'basic', + executor, + }); + mockedLicenseState.ensureLicenseForActionType.mockImplementation(() => { + throw new Error('Fail'); + }); + savedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'action', + attributes: { + actionTypeId: 'my-action-type', + }, + references: [], + }); + savedObjectsClient.update.mockResolvedValueOnce({ + id: 'my-action', + type: 'action', + attributes: { + actionTypeId: 'my-action-type', + name: 'my name', + config: {}, + secrets: {}, + }, + references: [], + }); + await expect( + actionsClient.update({ + id: 'my-action', + action: { + name: 'my name', + config: {}, + secrets: {}, + }, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot(`"Fail"`); + }); }); diff --git a/x-pack/plugins/actions/server/actions_client.ts b/x-pack/plugins/actions/server/actions_client.ts index a06048953b62c..129829850f9c1 100644 --- a/x-pack/plugins/actions/server/actions_client.ts +++ b/x-pack/plugins/actions/server/actions_client.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import Boom from 'boom'; import { IScopedClusterClient, SavedObjectsClientContract, @@ -93,11 +92,7 @@ export class ActionsClient { const validatedActionTypeConfig = validateConfig(actionType, config); const validatedActionTypeSecrets = validateSecrets(actionType, secrets); - try { - this.actionTypeRegistry.ensureActionTypeEnabled(actionTypeId); - } catch (err) { - throw Boom.badRequest(err.message); - } + this.actionTypeRegistry.ensureActionTypeEnabled(actionTypeId); const result = await this.savedObjectsClient.create('action', { actionTypeId, @@ -125,6 +120,8 @@ export class ActionsClient { const validatedActionTypeConfig = validateConfig(actionType, config); const validatedActionTypeSecrets = validateSecrets(actionType, secrets); + this.actionTypeRegistry.ensureActionTypeEnabled(actionTypeId); + const result = await this.savedObjectsClient.update('action', id, { actionTypeId, name, diff --git a/x-pack/plugins/actions/server/actions_config.mock.ts b/x-pack/plugins/actions/server/actions_config.mock.ts index b4e0324f9fead..addd35ae4f5f3 100644 --- a/x-pack/plugins/actions/server/actions_config.mock.ts +++ b/x-pack/plugins/actions/server/actions_config.mock.ts @@ -6,11 +6,18 @@ import { ActionsConfigurationUtilities } from './actions_config'; -export const configUtilsMock: ActionsConfigurationUtilities = { - isWhitelistedHostname: _ => true, - isWhitelistedUri: _ => true, - isActionTypeEnabled: _ => true, - ensureWhitelistedHostname: _ => {}, - ensureWhitelistedUri: _ => {}, - ensureActionTypeEnabled: _ => {}, +const createActionsConfigMock = () => { + const mocked: jest.Mocked = { + isWhitelistedHostname: jest.fn().mockReturnValue(true), + isWhitelistedUri: jest.fn().mockReturnValue(true), + isActionTypeEnabled: jest.fn().mockReturnValue(true), + ensureWhitelistedHostname: jest.fn().mockReturnValue({}), + ensureWhitelistedUri: jest.fn().mockReturnValue({}), + ensureActionTypeEnabled: jest.fn().mockReturnValue({}), + }; + return mocked; +}; + +export const actionsConfigMock = { + create: createActionsConfigMock, }; diff --git a/x-pack/plugins/actions/server/actions_config.ts b/x-pack/plugins/actions/server/actions_config.ts index e589969c50e54..64d1fd7fe90ac 100644 --- a/x-pack/plugins/actions/server/actions_config.ts +++ b/x-pack/plugins/actions/server/actions_config.ts @@ -11,6 +11,7 @@ import { curry } from 'lodash'; import { pipe } from 'fp-ts/lib/pipeable'; import { ActionsConfigType } from './types'; +import { ActionTypeDisabledError } from './lib'; export enum WhitelistedHosts { Any = '*', @@ -103,7 +104,7 @@ export function getActionsConfigurationUtilities( }, ensureActionTypeEnabled(actionType: string) { if (!isActionTypeEnabled(actionType)) { - throw new Error(disabledActionTypeErrorMessage(actionType)); + throw new ActionTypeDisabledError(disabledActionTypeErrorMessage(actionType), 'config'); } }, }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/email.test.ts b/x-pack/plugins/actions/server/builtin_action_types/email.test.ts index 4ad4fe96f3447..0bd3992de30e6 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/email.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/email.test.ts @@ -12,7 +12,7 @@ import { Logger } from '../../../../../src/core/server'; import { savedObjectsClientMock } from '../../../../../src/core/server/mocks'; import { ActionType, ActionTypeExecutorOptions } from '../types'; -import { configUtilsMock } from '../actions_config.mock'; +import { actionsConfigMock } from '../actions_config.mock'; import { validateConfig, validateSecrets, validateParams } from '../lib'; import { createActionTypeRegistry } from './index.test'; import { sendEmail } from './lib/send_email'; @@ -37,13 +37,10 @@ const services = { let actionType: ActionType; let mockedLogger: jest.Mocked; -beforeAll(() => { - const { actionTypeRegistry } = createActionTypeRegistry(); - actionType = actionTypeRegistry.get(ACTION_TYPE_ID); -}); - beforeEach(() => { jest.resetAllMocks(); + const { actionTypeRegistry } = createActionTypeRegistry(); + actionType = actionTypeRegistry.get(ACTION_TYPE_ID); }); describe('actionTypeRegistry.get() works', () => { @@ -128,7 +125,7 @@ describe('config validation', () => { actionType = getActionType({ logger: mockedLogger, configurationUtilities: { - ...configUtilsMock, + ...actionsConfigMock.create(), isWhitelistedHostname: hostname => hostname === NODEMAILER_AOL_SERVICE_HOST, }, }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/email.ts b/x-pack/plugins/actions/server/builtin_action_types/email.ts index b209e7bbca6f7..16e0168a7deb9 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/email.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/email.ts @@ -118,6 +118,7 @@ export function getActionType(params: GetActionTypeParams): ActionType { const { logger, configurationUtilities } = params; return { id: '.email', + minimumLicenseRequired: 'gold', name: i18n.translate('xpack.actions.builtin.emailTitle', { defaultMessage: 'Email', }), diff --git a/x-pack/plugins/actions/server/builtin_action_types/es_index.ts b/x-pack/plugins/actions/server/builtin_action_types/es_index.ts index b1fe5e3af2d11..b86f0029b5383 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/es_index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/es_index.ts @@ -36,6 +36,7 @@ const ParamsSchema = schema.object({ export function getActionType({ logger }: { logger: Logger }): ActionType { return { id: '.index', + minimumLicenseRequired: 'basic', name: i18n.translate('xpack.actions.builtin.esIndexTitle', { defaultMessage: 'Index', }), diff --git a/x-pack/plugins/actions/server/builtin_action_types/index.test.ts b/x-pack/plugins/actions/server/builtin_action_types/index.test.ts index db6375fe18193..ac21905ede11c 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/index.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/index.test.ts @@ -10,7 +10,8 @@ import { taskManagerMock } from '../../../task_manager/server/task_manager.mock' import { registerBuiltInActionTypes } from './index'; import { Logger } from '../../../../../src/core/server'; import { loggingServiceMock } from '../../../../../src/core/server/mocks'; -import { configUtilsMock } from '../actions_config.mock'; +import { actionsConfigMock } from '../actions_config.mock'; +import { licenseStateMock } from '../lib/license_state.mock'; const ACTION_TYPE_IDS = ['.index', '.email', '.pagerduty', '.server-log', '.slack', '.webhook']; @@ -24,12 +25,13 @@ export function createActionTypeRegistry(): { taskRunnerFactory: new TaskRunnerFactory( new ActionExecutor({ isESOUsingEphemeralEncryptionKey: false }) ), - actionsConfigUtils: configUtilsMock, + actionsConfigUtils: actionsConfigMock.create(), + licenseState: licenseStateMock.create(), }); registerBuiltInActionTypes({ logger, actionTypeRegistry, - actionsConfigUtils: configUtilsMock, + actionsConfigUtils: actionsConfigMock.create(), }); return { logger, actionTypeRegistry }; } diff --git a/x-pack/plugins/actions/server/builtin_action_types/pagerduty.test.ts b/x-pack/plugins/actions/server/builtin_action_types/pagerduty.test.ts index caa183d665e09..514c9759d7b56 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/pagerduty.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/pagerduty.test.ts @@ -15,7 +15,7 @@ import { savedObjectsClientMock } from '../../../../../src/core/server/mocks'; import { postPagerduty } from './lib/post_pagerduty'; import { createActionTypeRegistry } from './index.test'; import { Logger } from '../../../../../src/core/server'; -import { configUtilsMock } from '../actions_config.mock'; +import { actionsConfigMock } from '../actions_config.mock'; const postPagerdutyMock = postPagerduty as jest.Mock; @@ -60,7 +60,7 @@ describe('validateConfig()', () => { actionType = getActionType({ logger: mockedLogger, configurationUtilities: { - ...configUtilsMock, + ...actionsConfigMock.create(), ensureWhitelistedUri: url => { expect(url).toEqual('https://events.pagerduty.com/v2/enqueue'); }, @@ -76,7 +76,7 @@ describe('validateConfig()', () => { actionType = getActionType({ logger: mockedLogger, configurationUtilities: { - ...configUtilsMock, + ...actionsConfigMock.create(), ensureWhitelistedUri: _ => { throw new Error(`target url is not whitelisted`); }, diff --git a/x-pack/plugins/actions/server/builtin_action_types/pagerduty.ts b/x-pack/plugins/actions/server/builtin_action_types/pagerduty.ts index 62f46d3d62503..2b607d0dd41ba 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/pagerduty.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/pagerduty.ts @@ -96,6 +96,7 @@ export function getActionType({ }): ActionType { return { id: '.pagerduty', + minimumLicenseRequired: 'gold', name: i18n.translate('xpack.actions.builtin.pagerdutyTitle', { defaultMessage: 'PagerDuty', }), diff --git a/x-pack/plugins/actions/server/builtin_action_types/server_log.ts b/x-pack/plugins/actions/server/builtin_action_types/server_log.ts index 01355f2a34f92..bf8a3d8032cc5 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/server_log.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/server_log.ts @@ -35,6 +35,7 @@ const ParamsSchema = schema.object({ export function getActionType({ logger }: { logger: Logger }): ActionType { return { id: '.server-log', + minimumLicenseRequired: 'basic', name: i18n.translate('xpack.actions.builtin.serverLogTitle', { defaultMessage: 'Server log', }), diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts index 67d595cc3ec56..7eda7060df846 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts @@ -9,7 +9,7 @@ import { ActionType, Services, ActionTypeExecutorOptions } from '../../types'; import { validateConfig, validateSecrets, validateParams } from '../../lib'; import { savedObjectsClientMock } from '../../../../../../src/core/server/mocks'; import { createActionTypeRegistry } from '../index.test'; -import { configUtilsMock } from '../../actions_config.mock'; +import { actionsConfigMock } from '../../actions_config.mock'; import { ACTION_TYPE_ID } from './constants'; import * as i18n from './translations'; @@ -109,7 +109,7 @@ describe('validateConfig()', () => { test('should validate and pass when the servicenow url is whitelisted', () => { actionType = getActionType({ configurationUtilities: { - ...configUtilsMock, + ...actionsConfigMock.create(), ensureWhitelistedUri: url => { expect(url).toEqual(mockOptions.config.apiUrl); }, @@ -122,7 +122,7 @@ describe('validateConfig()', () => { test('config validation returns an error if the specified URL isnt whitelisted', () => { actionType = getActionType({ configurationUtilities: { - ...configUtilsMock, + ...actionsConfigMock.create(), ensureWhitelistedUri: _ => { throw new Error(`target url is not whitelisted`); }, diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts index f844bef6441ee..a63c2fd3a6ceb 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts @@ -56,6 +56,7 @@ export function getActionType({ return { id: ACTION_TYPE_ID, name: i18n.NAME, + minimumLicenseRequired: 'platinum', validate: { config: schema.object(ConfigSchemaProps, { validate: curry(validateConfig)(configurationUtilities), diff --git a/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts b/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts index 919f0800c291c..49b0b84e9dbb5 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts @@ -8,7 +8,7 @@ import { ActionType, Services, ActionTypeExecutorOptions } from '../types'; import { savedObjectsClientMock } from '../../../../../src/core/server/mocks'; import { validateParams, validateSecrets } from '../lib'; import { getActionType } from './slack'; -import { configUtilsMock } from '../actions_config.mock'; +import { actionsConfigMock } from '../actions_config.mock'; const ACTION_TYPE_ID = '.slack'; @@ -22,7 +22,7 @@ let actionType: ActionType; beforeAll(() => { actionType = getActionType({ async executor(options: ActionTypeExecutorOptions): Promise {}, - configurationUtilities: configUtilsMock, + configurationUtilities: actionsConfigMock.create(), }); }); @@ -85,7 +85,7 @@ describe('validateActionTypeSecrets()', () => { test('should validate and pass when the slack webhookUrl is whitelisted', () => { actionType = getActionType({ configurationUtilities: { - ...configUtilsMock, + ...actionsConfigMock.create(), ensureWhitelistedUri: url => { expect(url).toEqual('https://api.slack.com/'); }, @@ -100,7 +100,7 @@ describe('validateActionTypeSecrets()', () => { test('config validation returns an error if the specified URL isnt whitelisted', () => { actionType = getActionType({ configurationUtilities: { - ...configUtilsMock, + ...actionsConfigMock.create(), ensureWhitelistedHostname: url => { throw new Error(`target hostname is not whitelisted`); }, @@ -135,7 +135,7 @@ describe('execute()', () => { actionType = getActionType({ executor: mockSlackExecutor, - configurationUtilities: configUtilsMock, + configurationUtilities: actionsConfigMock.create(), }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/slack.ts b/x-pack/plugins/actions/server/builtin_action_types/slack.ts index 3a351853c1e46..e51ef3f67bd65 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/slack.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/slack.ts @@ -50,6 +50,7 @@ export function getActionType({ }): ActionType { return { id: '.slack', + minimumLicenseRequired: 'gold', name: i18n.translate('xpack.actions.builtin.slackTitle', { defaultMessage: 'Slack', }), diff --git a/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts b/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts index d8f75de781841..03658b3b1dd85 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts @@ -12,7 +12,7 @@ import { getActionType } from './webhook'; import { ActionType, Services } from '../types'; import { validateConfig, validateSecrets, validateParams } from '../lib'; import { savedObjectsClientMock } from '../../../../../src/core/server/mocks'; -import { configUtilsMock } from '../actions_config.mock'; +import { actionsConfigMock } from '../actions_config.mock'; import { createActionTypeRegistry } from './index.test'; import { Logger } from '../../../../../src/core/server'; import axios from 'axios'; @@ -164,7 +164,7 @@ describe('config validation', () => { actionType = getActionType({ logger: mockedLogger, configurationUtilities: { - ...configUtilsMock, + ...actionsConfigMock.create(), ensureWhitelistedUri: _ => { throw new Error(`target url is not whitelisted`); }, @@ -207,7 +207,7 @@ describe('execute()', () => { axiosRequestMock.mockReset(); actionType = getActionType({ logger: mockedLogger, - configurationUtilities: configUtilsMock, + configurationUtilities: actionsConfigMock.create(), }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/webhook.ts b/x-pack/plugins/actions/server/builtin_action_types/webhook.ts index e275deace0dcc..6173edc2df15a 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/webhook.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/webhook.ts @@ -66,6 +66,7 @@ export function getActionType({ }): ActionType { return { id: '.webhook', + minimumLicenseRequired: 'gold', name: i18n.translate('xpack.actions.builtin.webhookTitle', { defaultMessage: 'Webhook', }), diff --git a/x-pack/plugins/actions/server/create_execute_function.test.ts b/x-pack/plugins/actions/server/create_execute_function.test.ts index 6d2a234639532..68c3967359ff4 100644 --- a/x-pack/plugins/actions/server/create_execute_function.test.ts +++ b/x-pack/plugins/actions/server/create_execute_function.test.ts @@ -7,6 +7,7 @@ import { taskManagerMock } from '../../task_manager/server/task_manager.mock'; import { createExecuteFunction } from './create_execute_function'; import { savedObjectsClientMock } from '../../../../src/core/server/mocks'; +import { actionTypeRegistryMock } from './action_type_registry.mock'; const mockTaskManager = taskManagerMock.start(); const savedObjectsClient = savedObjectsClientMock.create(); @@ -19,6 +20,7 @@ describe('execute()', () => { const executeFn = createExecuteFunction({ getBasePath, taskManager: mockTaskManager, + actionTypeRegistry: actionTypeRegistryMock.create(), getScopedSavedObjectsClient: jest.fn().mockReturnValueOnce(savedObjectsClient), isESOUsingEphemeralEncryptionKey: false, }); @@ -73,6 +75,7 @@ describe('execute()', () => { taskManager: mockTaskManager, getScopedSavedObjectsClient, isESOUsingEphemeralEncryptionKey: false, + actionTypeRegistry: actionTypeRegistryMock.create(), }); savedObjectsClient.get.mockResolvedValueOnce({ id: '123', @@ -121,6 +124,7 @@ describe('execute()', () => { taskManager: mockTaskManager, getScopedSavedObjectsClient, isESOUsingEphemeralEncryptionKey: false, + actionTypeRegistry: actionTypeRegistryMock.create(), }); savedObjectsClient.get.mockResolvedValueOnce({ id: '123', @@ -166,6 +170,7 @@ describe('execute()', () => { taskManager: mockTaskManager, getScopedSavedObjectsClient, isESOUsingEphemeralEncryptionKey: true, + actionTypeRegistry: actionTypeRegistryMock.create(), }); await expect( executeFn({ @@ -178,4 +183,36 @@ describe('execute()', () => { `"Unable to execute action due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml"` ); }); + + test('should ensure action type is enabled', async () => { + const mockedActionTypeRegistry = actionTypeRegistryMock.create(); + const getScopedSavedObjectsClient = jest.fn().mockReturnValueOnce(savedObjectsClient); + const executeFn = createExecuteFunction({ + getBasePath, + taskManager: mockTaskManager, + getScopedSavedObjectsClient, + isESOUsingEphemeralEncryptionKey: false, + actionTypeRegistry: mockedActionTypeRegistry, + }); + mockedActionTypeRegistry.ensureActionTypeEnabled.mockImplementation(() => { + throw new Error('Fail'); + }); + savedObjectsClient.get.mockResolvedValueOnce({ + id: '123', + type: 'action', + attributes: { + actionTypeId: 'mock-action', + }, + references: [], + }); + + await expect( + executeFn({ + id: '123', + params: { baz: false }, + spaceId: 'default', + apiKey: null, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot(`"Fail"`); + }); }); diff --git a/x-pack/plugins/actions/server/create_execute_function.ts b/x-pack/plugins/actions/server/create_execute_function.ts index 5316e833f33d9..4bbcda4cba7fc 100644 --- a/x-pack/plugins/actions/server/create_execute_function.ts +++ b/x-pack/plugins/actions/server/create_execute_function.ts @@ -6,13 +6,14 @@ import { SavedObjectsClientContract } from '../../../../src/core/server'; import { TaskManagerStartContract } from '../../task_manager/server'; -import { GetBasePathFunction, RawAction } from './types'; +import { GetBasePathFunction, RawAction, ActionTypeRegistryContract } from './types'; interface CreateExecuteFunctionOptions { taskManager: TaskManagerStartContract; getScopedSavedObjectsClient: (request: any) => SavedObjectsClientContract; getBasePath: GetBasePathFunction; isESOUsingEphemeralEncryptionKey: boolean; + actionTypeRegistry: ActionTypeRegistryContract; } export interface ExecuteOptions { @@ -25,6 +26,7 @@ export interface ExecuteOptions { export function createExecuteFunction({ getBasePath, taskManager, + actionTypeRegistry, getScopedSavedObjectsClient, isESOUsingEphemeralEncryptionKey, }: CreateExecuteFunctionOptions) { @@ -60,6 +62,9 @@ export function createExecuteFunction({ const savedObjectsClient = getScopedSavedObjectsClient(fakeRequest); const actionSavedObject = await savedObjectsClient.get('action', id); + + actionTypeRegistry.ensureActionTypeEnabled(actionSavedObject.attributes.actionTypeId); + const actionTaskParamsRecord = await savedObjectsClient.create('action_task_params', { actionId: id, params, diff --git a/x-pack/plugins/actions/server/lib/action_executor.test.ts b/x-pack/plugins/actions/server/lib/action_executor.test.ts index 6ab5b812161c3..bbcb0457fc1d1 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.test.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.test.ts @@ -12,6 +12,7 @@ import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/serv import { savedObjectsClientMock, loggingServiceMock } from '../../../../../src/core/server/mocks'; import { eventLoggerMock } from '../../../event_log/server/mocks'; import { spacesServiceMock } from '../../../spaces/server/spaces_service/spaces_service.mock'; +import { ActionType } from '../types'; const actionExecutor = new ActionExecutor({ isESOUsingEphemeralEncryptionKey: false }); const savedObjectsClient = savedObjectsClientMock.create(); @@ -50,9 +51,10 @@ beforeEach(() => { }); test('successfully executes', async () => { - const actionType = { + const actionType: jest.Mocked = { id: 'test', name: 'Test', + minimumLicenseRequired: 'basic', executor: jest.fn(), }; const actionSavedObject = { @@ -96,9 +98,10 @@ test('successfully executes', async () => { }); test('provides empty config when config and / or secrets is empty', async () => { - const actionType = { + const actionType: jest.Mocked = { id: 'test', name: 'Test', + minimumLicenseRequired: 'basic', executor: jest.fn(), }; const actionSavedObject = { @@ -120,9 +123,10 @@ test('provides empty config when config and / or secrets is empty', async () => }); test('throws an error when config is invalid', async () => { - const actionType = { + const actionType: jest.Mocked = { id: 'test', name: 'Test', + minimumLicenseRequired: 'basic', validate: { config: schema.object({ param1: schema.string(), @@ -152,9 +156,10 @@ test('throws an error when config is invalid', async () => { }); test('throws an error when params is invalid', async () => { - const actionType = { + const actionType: jest.Mocked = { id: 'test', name: 'Test', + minimumLicenseRequired: 'basic', validate: { params: schema.object({ param1: schema.string(), @@ -190,10 +195,11 @@ test('throws an error when failing to load action through savedObjectsClient', a ); }); -test('returns an error if actionType is not enabled', async () => { - const actionType = { +test('throws an error if actionType is not enabled', async () => { + const actionType: jest.Mocked = { id: 'test', name: 'Test', + minimumLicenseRequired: 'basic', executor: jest.fn(), }; const actionSavedObject = { @@ -210,17 +216,11 @@ test('returns an error if actionType is not enabled', async () => { actionTypeRegistry.ensureActionTypeEnabled.mockImplementationOnce(() => { throw new Error('not enabled for test'); }); - const result = await actionExecutor.execute(executeParams); + await expect(actionExecutor.execute(executeParams)).rejects.toThrowErrorMatchingInlineSnapshot( + `"not enabled for test"` + ); expect(actionTypeRegistry.ensureActionTypeEnabled).toHaveBeenCalledWith('test'); - expect(result).toMatchInlineSnapshot(` - Object { - "actionId": "1", - "message": "not enabled for test", - "retry": false, - "status": "error", - } - `); }); test('throws an error when passing isESOUsingEphemeralEncryptionKey with value of true', async () => { diff --git a/x-pack/plugins/actions/server/lib/action_executor.ts b/x-pack/plugins/actions/server/lib/action_executor.ts index e42a69812b7da..af0353247d99f 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.ts @@ -82,11 +82,7 @@ export class ActionExecutor { attributes: { actionTypeId, config, name }, } = await services.savedObjectsClient.get('action', actionId); - try { - actionTypeRegistry.ensureActionTypeEnabled(actionTypeId); - } catch (err) { - return { status: 'error', actionId, message: err.message, retry: false }; - } + actionTypeRegistry.ensureActionTypeEnabled(actionTypeId); // Only get encrypted attributes here, the remaining attributes can be fetched in // the savedObjectsClient call diff --git a/x-pack/plugins/actions/server/lib/errors/action_type_disabled.ts b/x-pack/plugins/actions/server/lib/errors/action_type_disabled.ts new file mode 100644 index 0000000000000..fb15125fa6957 --- /dev/null +++ b/x-pack/plugins/actions/server/lib/errors/action_type_disabled.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { KibanaResponseFactory } from '../../../../../../src/core/server'; +import { ErrorThatHandlesItsOwnResponse } from './types'; + +export type ActionTypeDisabledReason = + | 'config' + | 'license_unavailable' + | 'license_invalid' + | 'license_expired'; + +export class ActionTypeDisabledError extends Error implements ErrorThatHandlesItsOwnResponse { + public readonly reason: ActionTypeDisabledReason; + + constructor(message: string, reason: ActionTypeDisabledReason) { + super(message); + this.reason = reason; + } + + public sendResponse(res: KibanaResponseFactory) { + return res.forbidden({ body: { message: this.message } }); + } +} diff --git a/x-pack/plugins/actions/server/lib/errors/index.ts b/x-pack/plugins/actions/server/lib/errors/index.ts new file mode 100644 index 0000000000000..79c6d53c403ff --- /dev/null +++ b/x-pack/plugins/actions/server/lib/errors/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ErrorThatHandlesItsOwnResponse } from './types'; + +export function isErrorThatHandlesItsOwnResponse( + e: ErrorThatHandlesItsOwnResponse +): e is ErrorThatHandlesItsOwnResponse { + return typeof (e as ErrorThatHandlesItsOwnResponse).sendResponse === 'function'; +} + +export { ActionTypeDisabledError, ActionTypeDisabledReason } from './action_type_disabled'; diff --git a/x-pack/plugins/actions/server/lib/errors/types.ts b/x-pack/plugins/actions/server/lib/errors/types.ts new file mode 100644 index 0000000000000..949dc348265ae --- /dev/null +++ b/x-pack/plugins/actions/server/lib/errors/types.ts @@ -0,0 +1,11 @@ +/* + * 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 { KibanaResponseFactory, IKibanaResponse } from '../../../../../../src/core/server'; + +export interface ErrorThatHandlesItsOwnResponse extends Error { + sendResponse(res: KibanaResponseFactory): IKibanaResponse; +} diff --git a/x-pack/plugins/actions/server/lib/index.ts b/x-pack/plugins/actions/server/lib/index.ts index 0667e0548646e..f03b6de1fc5fb 100644 --- a/x-pack/plugins/actions/server/lib/index.ts +++ b/x-pack/plugins/actions/server/lib/index.ts @@ -8,3 +8,10 @@ export { ExecutorError } from './executor_error'; export { validateParams, validateConfig, validateSecrets } from './validate_with_schema'; export { TaskRunnerFactory } from './task_runner_factory'; export { ActionExecutor, ActionExecutorContract } from './action_executor'; +export { ILicenseState, LicenseState } from './license_state'; +export { verifyApiAccess } from './verify_api_access'; +export { + ActionTypeDisabledError, + ActionTypeDisabledReason, + isErrorThatHandlesItsOwnResponse, +} from './errors'; diff --git a/x-pack/plugins/actions/server/lib/license_state.mock.ts b/x-pack/plugins/actions/server/lib/license_state.mock.ts index f36f3a9eaeade..72a21f878a150 100644 --- a/x-pack/plugins/actions/server/lib/license_state.mock.ts +++ b/x-pack/plugins/actions/server/lib/license_state.mock.ts @@ -4,35 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ -import { of } from 'rxjs'; -import { LicenseState } from './license_state'; -import { LICENSE_CHECK_STATE, ILicense } from '../../../licensing/server'; +import { ILicenseState } from './license_state'; +import { LICENSE_CHECK_STATE } from '../../../licensing/server'; -export const mockLicenseState = () => { - const license: ILicense = { - uid: '123', - status: 'active', - isActive: true, - signature: 'sig', - isAvailable: true, - toJSON: () => ({ - signature: 'sig', +export const createLicenseStateMock = () => { + const licenseState: jest.Mocked = { + clean: jest.fn(), + getLicenseInformation: jest.fn(), + ensureLicenseForActionType: jest.fn(), + isLicenseValidForActionType: jest.fn(), + checkLicense: jest.fn().mockResolvedValue({ + state: LICENSE_CHECK_STATE.Valid, }), - getUnavailableReason: () => undefined, - hasAtLeast() { - return true; - }, - check() { - return { - state: LICENSE_CHECK_STATE.Valid, - }; - }, - getFeature() { - return { - isAvailable: true, - isEnabled: true, - }; - }, }; - return new LicenseState(of(license)); + return licenseState; +}; + +export const licenseStateMock = { + create: createLicenseStateMock, }; diff --git a/x-pack/plugins/actions/server/lib/license_state.test.ts b/x-pack/plugins/actions/server/lib/license_state.test.ts index dbb70857dad5c..ba1fbcb83464a 100644 --- a/x-pack/plugins/actions/server/lib/license_state.test.ts +++ b/x-pack/plugins/actions/server/lib/license_state.test.ts @@ -4,12 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; -import { LicenseState } from './license_state'; +import { ActionType } from '../types'; +import { BehaviorSubject } from 'rxjs'; +import { LicenseState, ILicenseState } from './license_state'; import { licensingMock } from '../../../licensing/server/mocks'; -import { LICENSE_CHECK_STATE } from '../../../licensing/server'; +import { LICENSE_CHECK_STATE, ILicense } from '../../../licensing/server'; -describe('license_state', () => { +describe('checkLicense()', () => { let getRawLicense: any; beforeEach(() => { @@ -29,7 +30,7 @@ describe('license_state', () => { const licensing = licensingMock.createSetup(); const licenseState = new LicenseState(licensing.license$); const actionsLicenseInfo = licenseState.checkLicense(getRawLicense()); - expect(actionsLicenseInfo.enableAppLink).to.be(false); + expect(actionsLicenseInfo.enableAppLink).toBe(false); }); }); @@ -46,7 +47,131 @@ describe('license_state', () => { const licensing = licensingMock.createSetup(); const licenseState = new LicenseState(licensing.license$); const actionsLicenseInfo = licenseState.checkLicense(getRawLicense()); - expect(actionsLicenseInfo.showAppLink).to.be(true); + expect(actionsLicenseInfo.showAppLink).toBe(true); }); }); }); + +describe('isLicenseValidForActionType', () => { + let license: BehaviorSubject; + let licenseState: ILicenseState; + const fooActionType: ActionType = { + id: 'foo', + name: 'Foo', + minimumLicenseRequired: 'gold', + executor: async () => {}, + }; + + beforeEach(() => { + license = new BehaviorSubject(null as any); + licenseState = new LicenseState(license); + }); + + test('should return false when license not defined', () => { + expect(licenseState.isLicenseValidForActionType(fooActionType)).toEqual({ + isValid: false, + reason: 'unavailable', + }); + }); + + test('should return false when license not available', () => { + license.next({ isAvailable: false } as any); + expect(licenseState.isLicenseValidForActionType(fooActionType)).toEqual({ + isValid: false, + reason: 'unavailable', + }); + }); + + test('should return false when license is expired', () => { + const expiredLicense = licensingMock.createLicense({ license: { status: 'expired' } }); + license.next(expiredLicense); + expect(licenseState.isLicenseValidForActionType(fooActionType)).toEqual({ + isValid: false, + reason: 'expired', + }); + }); + + test('should return false when license is invalid', () => { + const basicLicense = licensingMock.createLicense({ + license: { status: 'active', type: 'basic' }, + }); + license.next(basicLicense); + expect(licenseState.isLicenseValidForActionType(fooActionType)).toEqual({ + isValid: false, + reason: 'invalid', + }); + }); + + test('should return true when license is valid', () => { + const goldLicense = licensingMock.createLicense({ + license: { status: 'active', type: 'gold' }, + }); + license.next(goldLicense); + expect(licenseState.isLicenseValidForActionType(fooActionType)).toEqual({ + isValid: true, + }); + }); +}); + +describe('ensureLicenseForActionType()', () => { + let license: BehaviorSubject; + let licenseState: ILicenseState; + const fooActionType: ActionType = { + id: 'foo', + name: 'Foo', + minimumLicenseRequired: 'gold', + executor: async () => {}, + }; + + beforeEach(() => { + license = new BehaviorSubject(null as any); + licenseState = new LicenseState(license); + }); + + test('should throw when license not defined', () => { + expect(() => + licenseState.ensureLicenseForActionType(fooActionType) + ).toThrowErrorMatchingInlineSnapshot( + `"Action type foo is disabled because license information is not available at this time."` + ); + }); + + test('should throw when license not available', () => { + license.next({ isAvailable: false } as any); + expect(() => + licenseState.ensureLicenseForActionType(fooActionType) + ).toThrowErrorMatchingInlineSnapshot( + `"Action type foo is disabled because license information is not available at this time."` + ); + }); + + test('should throw when license is expired', () => { + const expiredLicense = licensingMock.createLicense({ license: { status: 'expired' } }); + license.next(expiredLicense); + expect(() => + licenseState.ensureLicenseForActionType(fooActionType) + ).toThrowErrorMatchingInlineSnapshot( + `"Action type foo is disabled because your basic license has expired."` + ); + }); + + test('should throw when license is invalid', () => { + const basicLicense = licensingMock.createLicense({ + license: { status: 'active', type: 'basic' }, + }); + license.next(basicLicense); + expect(() => + licenseState.ensureLicenseForActionType(fooActionType) + ).toThrowErrorMatchingInlineSnapshot( + `"Action type foo is disabled because your basic license does not support it. Please upgrade your license."` + ); + }); + + test('should not throw when license is valid', () => { + const goldLicense = licensingMock.createLicense({ + license: { status: 'active', type: 'gold' }, + }); + license.next(goldLicense); + licenseState.ensureLicenseForActionType(fooActionType); + }); +}); diff --git a/x-pack/plugins/actions/server/lib/license_state.ts b/x-pack/plugins/actions/server/lib/license_state.ts index 7b25e55ac0ba1..9d87818805dcf 100644 --- a/x-pack/plugins/actions/server/lib/license_state.ts +++ b/x-pack/plugins/actions/server/lib/license_state.ts @@ -9,6 +9,10 @@ import { Observable, Subscription } from 'rxjs'; import { assertNever } from '../../../../../src/core/utils'; import { ILicense, LICENSE_CHECK_STATE } from '../../../licensing/common/types'; import { PLUGIN } from '../constants/plugin'; +import { ActionType } from '../types'; +import { ActionTypeDisabledError } from './errors'; + +export type ILicenseState = PublicMethodsOf; export interface ActionsLicenseInformation { showAppLink: boolean; @@ -19,12 +23,14 @@ export interface ActionsLicenseInformation { export class LicenseState { private licenseInformation: ActionsLicenseInformation = this.checkLicense(undefined); private subscription: Subscription; + private license?: ILicense; constructor(license$: Observable) { this.subscription = license$.subscribe(this.updateInformation.bind(this)); } private updateInformation(license: ILicense | undefined) { + this.license = license; this.licenseInformation = this.checkLicense(license); } @@ -36,6 +42,71 @@ export class LicenseState { return this.licenseInformation; } + public isLicenseValidForActionType( + actionType: ActionType + ): { isValid: true } | { isValid: false; reason: 'unavailable' | 'expired' | 'invalid' } { + if (!this.license?.isAvailable) { + return { isValid: false, reason: 'unavailable' }; + } + + const check = this.license.check(actionType.id, actionType.minimumLicenseRequired); + + switch (check.state) { + case LICENSE_CHECK_STATE.Expired: + return { isValid: false, reason: 'expired' }; + case LICENSE_CHECK_STATE.Invalid: + return { isValid: false, reason: 'invalid' }; + case LICENSE_CHECK_STATE.Unavailable: + return { isValid: false, reason: 'unavailable' }; + case LICENSE_CHECK_STATE.Valid: + return { isValid: true }; + default: + return assertNever(check.state); + } + } + + public ensureLicenseForActionType(actionType: ActionType) { + const check = this.isLicenseValidForActionType(actionType); + + if (check.isValid) { + return; + } + + switch (check.reason) { + case 'unavailable': + throw new ActionTypeDisabledError( + i18n.translate('xpack.actions.serverSideErrors.unavailableLicenseErrorMessage', { + defaultMessage: + 'Action type {actionTypeId} is disabled because license information is not available at this time.', + values: { + actionTypeId: actionType.id, + }, + }), + 'license_unavailable' + ); + case 'expired': + throw new ActionTypeDisabledError( + i18n.translate('xpack.actions.serverSideErrors.expirerdLicenseErrorMessage', { + defaultMessage: + 'Action type {actionTypeId} is disabled because your {licenseType} license has expired.', + values: { actionTypeId: actionType.id, licenseType: this.license!.type }, + }), + 'license_expired' + ); + case 'invalid': + throw new ActionTypeDisabledError( + i18n.translate('xpack.actions.serverSideErrors.invalidLicenseErrorMessage', { + defaultMessage: + 'Action type {actionTypeId} is disabled because your {licenseType} license does not support it. Please upgrade your license.', + values: { actionTypeId: actionType.id, licenseType: this.license!.type }, + }), + 'license_invalid' + ); + default: + assertNever(check.reason); + } + } + public checkLicense(license: ILicense | undefined): ActionsLicenseInformation { if (!license?.isAvailable) { return { diff --git a/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts b/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts index 6be5e1f79ee82..43882cef21170 100644 --- a/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts +++ b/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts @@ -14,6 +14,7 @@ import { actionExecutorMock } from './action_executor.mock'; import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/server/mocks'; import { savedObjectsClientMock, loggingServiceMock } from 'src/core/server/mocks'; import { eventLoggerMock } from '../../../event_log/server/mocks'; +import { ActionTypeDisabledError } from './errors'; const spaceIdToNamespace = jest.fn(); const actionTypeRegistry = actionTypeRegistryMock.create(); @@ -63,6 +64,7 @@ const actionExecutorInitializerParams = { }; const taskRunnerFactoryInitializerParams = { spaceIdToNamespace, + actionTypeRegistry, logger: loggingServiceMock.create().get(), encryptedSavedObjectsPlugin: mockedEncryptedSavedObjectsPlugin, getBasePath: jest.fn().mockReturnValue(undefined), @@ -308,3 +310,32 @@ test(`doesn't use API key when not provided`, async () => { }, }); }); + +test(`throws an error when license doesn't support the action type`, async () => { + const taskRunner = taskRunnerFactory.create({ + taskInstance: mockedTaskInstance, + }); + + mockedEncryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mockResolvedValueOnce({ + id: '3', + type: 'action_task_params', + attributes: { + actionId: '2', + params: { baz: true }, + apiKey: Buffer.from('123:abc').toString('base64'), + }, + references: [], + }); + mockedActionExecutor.execute.mockImplementation(() => { + throw new ActionTypeDisabledError('Fail', 'license_invalid'); + }); + + try { + await taskRunner.run(); + throw new Error('Should have thrown'); + } catch (e) { + expect(e instanceof ExecutorError).toEqual(true); + expect(e.data).toEqual({}); + expect(e.retry).toEqual(false); + } +}); diff --git a/x-pack/plugins/actions/server/lib/task_runner_factory.ts b/x-pack/plugins/actions/server/lib/task_runner_factory.ts index c78b43f4ef3ba..e2a6128aea203 100644 --- a/x-pack/plugins/actions/server/lib/task_runner_factory.ts +++ b/x-pack/plugins/actions/server/lib/task_runner_factory.ts @@ -9,10 +9,18 @@ import { ExecutorError } from './executor_error'; import { Logger, CoreStart } from '../../../../../src/core/server'; import { RunContext } from '../../../task_manager/server'; import { EncryptedSavedObjectsPluginStart } from '../../../encrypted_saved_objects/server'; -import { ActionTaskParams, GetBasePathFunction, SpaceIdToNamespaceFunction } from '../types'; +import { ActionTypeDisabledError } from './errors'; +import { + ActionTaskParams, + ActionTypeRegistryContract, + GetBasePathFunction, + SpaceIdToNamespaceFunction, + ActionTypeExecutorResult, +} from '../types'; export interface TaskRunnerContext { logger: Logger; + actionTypeRegistry: ActionTypeRegistryContract; encryptedSavedObjectsPlugin: EncryptedSavedObjectsPluginStart; spaceIdToNamespace: SpaceIdToNamespaceFunction; getBasePath: GetBasePathFunction; @@ -85,11 +93,20 @@ export class TaskRunnerFactory { }, }; - const executorResult = await actionExecutor.execute({ - params, - actionId, - request: fakeRequest, - }); + let executorResult: ActionTypeExecutorResult; + try { + executorResult = await actionExecutor.execute({ + params, + actionId, + request: fakeRequest, + }); + } catch (e) { + if (e instanceof ActionTypeDisabledError) { + // We'll stop re-trying due to action being forbidden + throw new ExecutorError(e.message, {}, false); + } + throw e; + } if (executorResult.status === 'error') { // Task manager error handler only kicks in when an error thrown (at this time) diff --git a/x-pack/plugins/actions/server/lib/validate_with_schema.test.ts b/x-pack/plugins/actions/server/lib/validate_with_schema.test.ts index 28122c72baf65..b7d408985ed9f 100644 --- a/x-pack/plugins/actions/server/lib/validate_with_schema.test.ts +++ b/x-pack/plugins/actions/server/lib/validate_with_schema.test.ts @@ -14,7 +14,12 @@ const executor: ExecutorType = async options => { }; test('should validate when there are no validators', () => { - const actionType: ActionType = { id: 'foo', name: 'bar', executor }; + const actionType: ActionType = { + id: 'foo', + name: 'bar', + minimumLicenseRequired: 'basic', + executor, + }; const testValue = { any: ['old', 'thing'] }; const result = validateConfig(actionType, testValue); @@ -22,7 +27,13 @@ test('should validate when there are no validators', () => { }); test('should validate when there are no individual validators', () => { - const actionType: ActionType = { id: 'foo', name: 'bar', executor, validate: {} }; + const actionType: ActionType = { + id: 'foo', + name: 'bar', + minimumLicenseRequired: 'basic', + executor, + validate: {}, + }; let result; const testValue = { any: ['old', 'thing'] }; @@ -42,6 +53,7 @@ test('should validate when validators return incoming value', () => { const actionType: ActionType = { id: 'foo', name: 'bar', + minimumLicenseRequired: 'basic', executor, validate: { params: selfValidator, @@ -69,6 +81,7 @@ test('should validate when validators return different values', () => { const actionType: ActionType = { id: 'foo', name: 'bar', + minimumLicenseRequired: 'basic', executor, validate: { params: selfValidator, @@ -99,6 +112,7 @@ test('should throw with expected error when validators fail', () => { const actionType: ActionType = { id: 'foo', name: 'bar', + minimumLicenseRequired: 'basic', executor, validate: { params: erroringValidator, @@ -127,6 +141,7 @@ test('should work with @kbn/config-schema', () => { const actionType: ActionType = { id: 'foo', name: 'bar', + minimumLicenseRequired: 'basic', executor, validate: { params: testSchema, diff --git a/x-pack/plugins/actions/server/lib/license_api_access.ts b/x-pack/plugins/actions/server/lib/verify_api_access.ts similarity index 81% rename from x-pack/plugins/actions/server/lib/license_api_access.ts rename to x-pack/plugins/actions/server/lib/verify_api_access.ts index 2e650ebf5eb17..2055c66865c4e 100644 --- a/x-pack/plugins/actions/server/lib/license_api_access.ts +++ b/x-pack/plugins/actions/server/lib/verify_api_access.ts @@ -5,9 +5,9 @@ */ import Boom from 'boom'; -import { LicenseState } from './license_state'; +import { ILicenseState } from './license_state'; -export function verifyApiAccess(licenseState: LicenseState) { +export function verifyApiAccess(licenseState: ILicenseState) { const licenseCheckResults = licenseState.getLicenseInformation(); if (licenseCheckResults.showAppLink && licenseCheckResults.enableAppLink) { diff --git a/x-pack/plugins/actions/server/mocks.ts b/x-pack/plugins/actions/server/mocks.ts index 1f68d8d4a3a69..75396f2aad897 100644 --- a/x-pack/plugins/actions/server/mocks.ts +++ b/x-pack/plugins/actions/server/mocks.ts @@ -19,6 +19,7 @@ const createSetupMock = () => { const createStartMock = () => { const mock: jest.Mocked = { execute: jest.fn(), + isActionTypeEnabled: jest.fn(), getActionsClientWithRequest: jest.fn().mockResolvedValue(actionsClientMock.create()), }; return mock; diff --git a/x-pack/plugins/actions/server/plugin.test.ts b/x-pack/plugins/actions/server/plugin.test.ts index f55a5ca172144..383f84590fbc6 100644 --- a/x-pack/plugins/actions/server/plugin.test.ts +++ b/x-pack/plugins/actions/server/plugin.test.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ActionsPlugin, ActionsPluginsSetup, ActionsPluginsStart } from './plugin'; import { PluginInitializerContext } from '../../../../src/core/server'; import { coreMock, httpServerMock } from '../../../../src/core/server/mocks'; import { licensingMock } from '../../licensing/server/mocks'; @@ -12,6 +11,13 @@ import { encryptedSavedObjectsMock } from '../../encrypted_saved_objects/server/ import { taskManagerMock } from '../../task_manager/server/mocks'; import { eventLogMock } from '../../event_log/server/mocks'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { ActionType } from './types'; +import { + ActionsPlugin, + ActionsPluginsSetup, + ActionsPluginsStart, + PluginSetupContract, +} from './plugin'; describe('Actions Plugin', () => { const usageCollectionMock: jest.Mocked = ({ @@ -97,6 +103,54 @@ describe('Actions Plugin', () => { ); }); }); + + describe('registerType()', () => { + let setup: PluginSetupContract; + const sampleActionType: ActionType = { + id: 'test', + name: 'test', + minimumLicenseRequired: 'basic', + async executor() {}, + }; + + beforeEach(async () => { + setup = await plugin.setup(coreSetup, pluginsSetup); + }); + + it('should throw error when license type is invalid', async () => { + expect(() => + setup.registerType({ + ...sampleActionType, + minimumLicenseRequired: 'foo' as any, + }) + ).toThrowErrorMatchingInlineSnapshot(`"\\"foo\\" is not a valid license type"`); + }); + + it('should throw error when license type is less than gold', async () => { + expect(() => + setup.registerType({ + ...sampleActionType, + minimumLicenseRequired: 'basic', + }) + ).toThrowErrorMatchingInlineSnapshot( + `"Third party action type \\"test\\" can only set minimumLicenseRequired to a gold license or higher"` + ); + }); + + it('should not throw when license type is gold', async () => { + setup.registerType({ + ...sampleActionType, + minimumLicenseRequired: 'gold', + }); + }); + + it('should not throw when license type is higher than gold', async () => { + setup.registerType({ + ...sampleActionType, + minimumLicenseRequired: 'platinum', + }); + }); + }); }); describe('start()', () => { let plugin: ActionsPlugin; diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index 10826ce795757..c6c4f377ab618 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -26,11 +26,12 @@ import { } from '../../encrypted_saved_objects/server'; import { TaskManagerSetupContract, TaskManagerStartContract } from '../../task_manager/server'; import { LicensingPluginSetup } from '../../licensing/server'; +import { LICENSE_TYPE } from '../../licensing/common/types'; import { SpacesPluginSetup, SpacesServiceSetup } from '../../spaces/server'; import { ActionsConfig } from './config'; -import { Services } from './types'; -import { ActionExecutor, TaskRunnerFactory } from './lib'; +import { Services, ActionType } from './types'; +import { ActionExecutor, TaskRunnerFactory, LicenseState, ILicenseState } from './lib'; import { ActionsClient } from './actions_client'; import { ActionTypeRegistry } from './action_type_registry'; import { ExecuteOptions } from './create_execute_function'; @@ -49,7 +50,6 @@ import { listActionTypesRoute, executeActionRoute, } from './routes'; -import { LicenseState } from './lib/license_state'; import { IEventLogger, IEventLogService } from '../../event_log/server'; import { initializeActionsTelemetry, scheduleActionsTelemetry } from './usage/task'; @@ -60,10 +60,11 @@ export const EVENT_LOG_ACTIONS = { }; export interface PluginSetupContract { - registerType: ActionTypeRegistry['register']; + registerType: (actionType: ActionType) => void; } export interface PluginStartContract { + isActionTypeEnabled(id: string): boolean; execute(options: ExecuteOptions): Promise; getActionsClientWithRequest(request: KibanaRequest): Promise>; } @@ -91,7 +92,7 @@ export class ActionsPlugin implements Plugin, Plugi private taskRunnerFactory?: TaskRunnerFactory; private actionTypeRegistry?: ActionTypeRegistry; private actionExecutor?: ActionExecutor; - private licenseState: LicenseState | null = null; + private licenseState: ILicenseState | null = null; private spaces?: SpacesServiceSetup; private eventLogger?: IEventLogger; private isESOUsingEphemeralEncryptionKey?: boolean; @@ -115,6 +116,7 @@ export class ActionsPlugin implements Plugin, Plugi } public async setup(core: CoreSetup, plugins: ActionsPluginsSetup): Promise { + this.licenseState = new LicenseState(plugins.licensing.license$); this.isESOUsingEphemeralEncryptionKey = plugins.encryptedSavedObjects.usingEphemeralEncryptionKey; @@ -156,6 +158,7 @@ export class ActionsPlugin implements Plugin, Plugi taskRunnerFactory, taskManager: plugins.taskManager, actionsConfigUtils, + licenseState: this.licenseState, }); this.taskRunnerFactory = taskRunnerFactory; this.actionTypeRegistry = actionTypeRegistry; @@ -190,7 +193,6 @@ export class ActionsPlugin implements Plugin, Plugi ); // Routes - this.licenseState = new LicenseState(plugins.licensing.license$); const router = core.http.createRouter(); createActionRoute(router, this.licenseState); deleteActionRoute(router, this.licenseState); @@ -201,7 +203,17 @@ export class ActionsPlugin implements Plugin, Plugi executeActionRoute(router, this.licenseState, actionExecutor); return { - registerType: actionTypeRegistry.register.bind(actionTypeRegistry), + registerType: (actionType: ActionType) => { + if (!(actionType.minimumLicenseRequired in LICENSE_TYPE)) { + throw new Error(`"${actionType.minimumLicenseRequired}" is not a valid license type`); + } + if (LICENSE_TYPE[actionType.minimumLicenseRequired] < LICENSE_TYPE.gold) { + throw new Error( + `Third party action type "${actionType.id}" can only set minimumLicenseRequired to a gold license or higher` + ); + } + actionTypeRegistry.register(actionType); + }, }; } @@ -227,6 +239,7 @@ export class ActionsPlugin implements Plugin, Plugi taskRunnerFactory!.initialize({ logger, + actionTypeRegistry: actionTypeRegistry!, encryptedSavedObjectsPlugin: plugins.encryptedSavedObjects, getBasePath: this.getBasePath, spaceIdToNamespace: this.spaceIdToNamespace, @@ -238,10 +251,14 @@ export class ActionsPlugin implements Plugin, Plugi return { execute: createExecuteFunction({ taskManager: plugins.taskManager, + actionTypeRegistry: actionTypeRegistry!, getScopedSavedObjectsClient: core.savedObjects.getScopedClient, getBasePath: this.getBasePath, isESOUsingEphemeralEncryptionKey: isESOUsingEphemeralEncryptionKey!, }), + isActionTypeEnabled: id => { + return this.actionTypeRegistry!.isActionTypeEnabled(id); + }, // Ability to get an actions client from legacy code async getActionsClientWithRequest(request: KibanaRequest) { if (isESOUsingEphemeralEncryptionKey === true) { diff --git a/x-pack/plugins/actions/server/routes/create.test.ts b/x-pack/plugins/actions/server/routes/create.test.ts index 6f7ebf2735edd..22cf0dd7f8ace 100644 --- a/x-pack/plugins/actions/server/routes/create.test.ts +++ b/x-pack/plugins/actions/server/routes/create.test.ts @@ -5,11 +5,11 @@ */ import { createActionRoute } from './create'; import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; -import { mockLicenseState } from '../lib/license_state.mock'; -import { verifyApiAccess } from '../lib/license_api_access'; +import { licenseStateMock } from '../lib/license_state.mock'; +import { verifyApiAccess, ActionTypeDisabledError } from '../lib'; import { mockHandlerArguments } from './_mock_handler_arguments'; -jest.mock('../lib/license_api_access.ts', () => ({ +jest.mock('../lib/verify_api_access.ts', () => ({ verifyApiAccess: jest.fn(), })); @@ -19,7 +19,7 @@ beforeEach(() => { describe('createActionRoute', () => { it('creates an action with proper parameters', async () => { - const licenseState = mockLicenseState(); + const licenseState = licenseStateMock.create(); const router: RouterMock = mockRouter.create(); createActionRoute(router, licenseState); @@ -82,7 +82,7 @@ describe('createActionRoute', () => { }); it('ensures the license allows creating actions', async () => { - const licenseState = mockLicenseState(); + const licenseState = licenseStateMock.create(); const router: RouterMock = mockRouter.create(); createActionRoute(router, licenseState); @@ -106,7 +106,7 @@ describe('createActionRoute', () => { }); it('ensures the license check prevents creating actions', async () => { - const licenseState = mockLicenseState(); + const licenseState = licenseStateMock.create(); const router: RouterMock = mockRouter.create(); (verifyApiAccess as jest.Mock).mockImplementation(() => { @@ -132,4 +132,23 @@ describe('createActionRoute', () => { expect(verifyApiAccess).toHaveBeenCalledWith(licenseState); }); + + it('ensures the action type gets validated for the license', async () => { + const licenseState = licenseStateMock.create(); + const router: RouterMock = mockRouter.create(); + + createActionRoute(router, licenseState); + + const [, handler] = router.post.mock.calls[0]; + + const actionsClient = { + create: jest.fn().mockRejectedValue(new ActionTypeDisabledError('Fail', 'license_invalid')), + }; + + const [context, req, res] = mockHandlerArguments({ actionsClient }, {}, ['ok', 'forbidden']); + + await handler(context, req, res); + + expect(res.forbidden).toHaveBeenCalledWith({ body: { message: 'Fail' } }); + }); }); diff --git a/x-pack/plugins/actions/server/routes/create.ts b/x-pack/plugins/actions/server/routes/create.ts index 2150dc4076449..0456fa8667de3 100644 --- a/x-pack/plugins/actions/server/routes/create.ts +++ b/x-pack/plugins/actions/server/routes/create.ts @@ -13,8 +13,7 @@ import { KibanaResponseFactory, } from 'kibana/server'; import { ActionResult } from '../types'; -import { LicenseState } from '../lib/license_state'; -import { verifyApiAccess } from '../lib/license_api_access'; +import { ILicenseState, verifyApiAccess, isErrorThatHandlesItsOwnResponse } from '../lib'; export const bodySchema = schema.object({ name: schema.string(), @@ -23,7 +22,7 @@ export const bodySchema = schema.object({ secrets: schema.recordOf(schema.string(), schema.any(), { defaultValue: {} }), }); -export const createActionRoute = (router: IRouter, licenseState: LicenseState) => { +export const createActionRoute = (router: IRouter, licenseState: ILicenseState) => { router.post( { path: `/api/action`, @@ -46,10 +45,17 @@ export const createActionRoute = (router: IRouter, licenseState: LicenseState) = } const actionsClient = context.actions.getActionsClient(); const action = req.body; - const actionRes: ActionResult = await actionsClient.create({ action }); - return res.ok({ - body: actionRes, - }); + try { + const actionRes: ActionResult = await actionsClient.create({ action }); + return res.ok({ + body: actionRes, + }); + } catch (e) { + if (isErrorThatHandlesItsOwnResponse(e)) { + return e.sendResponse(res); + } + throw e; + } }) ); }; diff --git a/x-pack/plugins/actions/server/routes/delete.test.ts b/x-pack/plugins/actions/server/routes/delete.test.ts index e44f325413428..6fb526628cb02 100644 --- a/x-pack/plugins/actions/server/routes/delete.test.ts +++ b/x-pack/plugins/actions/server/routes/delete.test.ts @@ -5,11 +5,11 @@ */ import { deleteActionRoute } from './delete'; import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; -import { mockLicenseState } from '../lib/license_state.mock'; -import { verifyApiAccess } from '../lib/license_api_access'; +import { licenseStateMock } from '../lib/license_state.mock'; +import { verifyApiAccess } from '../lib'; import { mockHandlerArguments } from './_mock_handler_arguments'; -jest.mock('../lib/license_api_access.ts', () => ({ +jest.mock('../lib/verify_api_access.ts', () => ({ verifyApiAccess: jest.fn(), })); @@ -19,7 +19,7 @@ beforeEach(() => { describe('deleteActionRoute', () => { it('deletes an action with proper parameters', async () => { - const licenseState = mockLicenseState(); + const licenseState = licenseStateMock.create(); const router: RouterMock = mockRouter.create(); deleteActionRoute(router, licenseState); @@ -64,7 +64,7 @@ describe('deleteActionRoute', () => { }); it('ensures the license allows deleting actions', async () => { - const licenseState = mockLicenseState(); + const licenseState = licenseStateMock.create(); const router: RouterMock = mockRouter.create(); deleteActionRoute(router, licenseState); @@ -85,7 +85,7 @@ describe('deleteActionRoute', () => { }); it('ensures the license check prevents deleting actions', async () => { - const licenseState = mockLicenseState(); + const licenseState = licenseStateMock.create(); const router: RouterMock = mockRouter.create(); (verifyApiAccess as jest.Mock).mockImplementation(() => { diff --git a/x-pack/plugins/actions/server/routes/delete.ts b/x-pack/plugins/actions/server/routes/delete.ts index 8508137b97750..6635133f318b1 100644 --- a/x-pack/plugins/actions/server/routes/delete.ts +++ b/x-pack/plugins/actions/server/routes/delete.ts @@ -17,14 +17,13 @@ import { IKibanaResponse, KibanaResponseFactory, } from 'kibana/server'; -import { LicenseState } from '../lib/license_state'; -import { verifyApiAccess } from '../lib/license_api_access'; +import { ILicenseState, verifyApiAccess } from '../lib'; const paramSchema = schema.object({ id: schema.string(), }); -export const deleteActionRoute = (router: IRouter, licenseState: LicenseState) => { +export const deleteActionRoute = (router: IRouter, licenseState: ILicenseState) => { router.delete( { path: `/api/action/{id}`, diff --git a/x-pack/plugins/actions/server/routes/execute.test.ts b/x-pack/plugins/actions/server/routes/execute.test.ts index d8b57b2fb849a..3a3ed1257f576 100644 --- a/x-pack/plugins/actions/server/routes/execute.test.ts +++ b/x-pack/plugins/actions/server/routes/execute.test.ts @@ -6,12 +6,11 @@ import { executeActionRoute } from './execute'; import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; -import { mockLicenseState } from '../lib/license_state.mock'; -import { verifyApiAccess } from '../lib/license_api_access'; +import { licenseStateMock } from '../lib/license_state.mock'; import { mockHandlerArguments } from './_mock_handler_arguments'; -import { ActionExecutorContract } from '../lib'; +import { ActionExecutorContract, verifyApiAccess, ActionTypeDisabledError } from '../lib'; -jest.mock('../lib/license_api_access.ts', () => ({ +jest.mock('../lib/verify_api_access.ts', () => ({ verifyApiAccess: jest.fn(), })); @@ -21,7 +20,7 @@ beforeEach(() => { describe('executeActionRoute', () => { it('executes an action with proper parameters', async () => { - const licenseState = mockLicenseState(); + const licenseState = licenseStateMock.create(); const router: RouterMock = mockRouter.create(); const [context, req, res] = mockHandlerArguments( @@ -77,7 +76,7 @@ describe('executeActionRoute', () => { }); it('returns a "204 NO CONTENT" when the executor returns a nullish value', async () => { - const licenseState = mockLicenseState(); + const licenseState = licenseStateMock.create(); const router: RouterMock = mockRouter.create(); const [context, req, res] = mockHandlerArguments( @@ -115,7 +114,7 @@ describe('executeActionRoute', () => { }); it('ensures the license allows action execution', async () => { - const licenseState = mockLicenseState(); + const licenseState = licenseStateMock.create(); const router: RouterMock = mockRouter.create(); const [context, req, res] = mockHandlerArguments( @@ -147,7 +146,7 @@ describe('executeActionRoute', () => { }); it('ensures the license check prevents action execution', async () => { - const licenseState = mockLicenseState(); + const licenseState = licenseStateMock.create(); const router: RouterMock = mockRouter.create(); (verifyApiAccess as jest.Mock).mockImplementation(() => { @@ -181,4 +180,33 @@ describe('executeActionRoute', () => { expect(verifyApiAccess).toHaveBeenCalledWith(licenseState); }); + + it('ensures the action type gets validated for the license', async () => { + const licenseState = licenseStateMock.create(); + const router: RouterMock = mockRouter.create(); + + const [context, req, res] = mockHandlerArguments( + {}, + { + body: {}, + params: {}, + }, + ['ok', 'forbidden'] + ); + + const actionExecutor = { + initialize: jest.fn(), + execute: jest.fn().mockImplementation(() => { + throw new ActionTypeDisabledError('Fail', 'license_invalid'); + }), + } as jest.Mocked; + + executeActionRoute(router, licenseState, actionExecutor); + + const [, handler] = router.post.mock.calls[0]; + + await handler(context, req, res); + + expect(res.forbidden).toHaveBeenCalledWith({ body: { message: 'Fail' } }); + }); }); diff --git a/x-pack/plugins/actions/server/routes/execute.ts b/x-pack/plugins/actions/server/routes/execute.ts index afccee3b5e70e..78693b5bfcf23 100644 --- a/x-pack/plugins/actions/server/routes/execute.ts +++ b/x-pack/plugins/actions/server/routes/execute.ts @@ -11,8 +11,7 @@ import { IKibanaResponse, KibanaResponseFactory, } from 'kibana/server'; -import { LicenseState } from '../lib/license_state'; -import { verifyApiAccess } from '../lib/license_api_access'; +import { ILicenseState, verifyApiAccess, isErrorThatHandlesItsOwnResponse } from '../lib'; import { ActionExecutorContract } from '../lib'; import { ActionTypeExecutorResult } from '../types'; @@ -27,7 +26,7 @@ const bodySchema = schema.object({ export const executeActionRoute = ( router: IRouter, - licenseState: LicenseState, + licenseState: ILicenseState, actionExecutor: ActionExecutorContract ) => { router.post( @@ -49,16 +48,23 @@ export const executeActionRoute = ( verifyApiAccess(licenseState); const { params } = req.body; const { id } = req.params; - const body: ActionTypeExecutorResult = await actionExecutor.execute({ - params, - request: req, - actionId: id, - }); - return body - ? res.ok({ - body, - }) - : res.noContent(); + try { + const body: ActionTypeExecutorResult = await actionExecutor.execute({ + params, + request: req, + actionId: id, + }); + return body + ? res.ok({ + body, + }) + : res.noContent(); + } catch (e) { + if (isErrorThatHandlesItsOwnResponse(e)) { + return e.sendResponse(res); + } + throw e; + } }) ); }; diff --git a/x-pack/plugins/actions/server/routes/find.test.ts b/x-pack/plugins/actions/server/routes/find.test.ts index b51130b2640aa..1b130421fa71f 100644 --- a/x-pack/plugins/actions/server/routes/find.test.ts +++ b/x-pack/plugins/actions/server/routes/find.test.ts @@ -6,11 +6,11 @@ import { findActionRoute } from './find'; import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; -import { mockLicenseState } from '../lib/license_state.mock'; -import { verifyApiAccess } from '../lib/license_api_access'; +import { licenseStateMock } from '../lib/license_state.mock'; +import { verifyApiAccess } from '../lib'; import { mockHandlerArguments } from './_mock_handler_arguments'; -jest.mock('../lib/license_api_access.ts', () => ({ +jest.mock('../lib/verify_api_access.ts', () => ({ verifyApiAccess: jest.fn(), })); @@ -20,7 +20,7 @@ beforeEach(() => { describe('findActionRoute', () => { it('finds actions with proper parameters', async () => { - const licenseState = mockLicenseState(); + const licenseState = licenseStateMock.create(); const router: RouterMock = mockRouter.create(); findActionRoute(router, licenseState); @@ -93,7 +93,7 @@ describe('findActionRoute', () => { }); it('ensures the license allows finding actions', async () => { - const licenseState = mockLicenseState(); + const licenseState = licenseStateMock.create(); const router: RouterMock = mockRouter.create(); findActionRoute(router, licenseState); @@ -123,7 +123,7 @@ describe('findActionRoute', () => { }); it('ensures the license check prevents finding actions', async () => { - const licenseState = mockLicenseState(); + const licenseState = licenseStateMock.create(); const router: RouterMock = mockRouter.create(); (verifyApiAccess as jest.Mock).mockImplementation(() => { diff --git a/x-pack/plugins/actions/server/routes/find.ts b/x-pack/plugins/actions/server/routes/find.ts index 820dd32d710ae..700e70c65d5df 100644 --- a/x-pack/plugins/actions/server/routes/find.ts +++ b/x-pack/plugins/actions/server/routes/find.ts @@ -13,8 +13,7 @@ import { KibanaResponseFactory, } from 'kibana/server'; import { FindOptions } from '../../../alerting/server'; -import { LicenseState } from '../lib/license_state'; -import { verifyApiAccess } from '../lib/license_api_access'; +import { ILicenseState, verifyApiAccess } from '../lib'; // config definition const querySchema = schema.object({ @@ -41,7 +40,7 @@ const querySchema = schema.object({ filter: schema.maybe(schema.string()), }); -export const findActionRoute = (router: IRouter, licenseState: LicenseState) => { +export const findActionRoute = (router: IRouter, licenseState: ILicenseState) => { router.get( { path: `/api/action/_find`, diff --git a/x-pack/plugins/actions/server/routes/get.test.ts b/x-pack/plugins/actions/server/routes/get.test.ts index 8762a68b192f2..f4e834a5b767c 100644 --- a/x-pack/plugins/actions/server/routes/get.test.ts +++ b/x-pack/plugins/actions/server/routes/get.test.ts @@ -6,11 +6,11 @@ import { getActionRoute } from './get'; import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; -import { mockLicenseState } from '../lib/license_state.mock'; -import { verifyApiAccess } from '../lib/license_api_access'; +import { licenseStateMock } from '../lib/license_state.mock'; +import { verifyApiAccess } from '../lib'; import { mockHandlerArguments } from './_mock_handler_arguments'; -jest.mock('../lib/license_api_access.ts', () => ({ +jest.mock('../lib/verify_api_access.ts', () => ({ verifyApiAccess: jest.fn(), })); @@ -20,7 +20,7 @@ beforeEach(() => { describe('getActionRoute', () => { it('gets an action with proper parameters', async () => { - const licenseState = mockLicenseState(); + const licenseState = licenseStateMock.create(); const router: RouterMock = mockRouter.create(); getActionRoute(router, licenseState); @@ -74,7 +74,7 @@ describe('getActionRoute', () => { }); it('ensures the license allows getting actions', async () => { - const licenseState = mockLicenseState(); + const licenseState = licenseStateMock.create(); const router: RouterMock = mockRouter.create(); getActionRoute(router, licenseState); @@ -104,7 +104,7 @@ describe('getActionRoute', () => { }); it('ensures the license check prevents getting actions', async () => { - const licenseState = mockLicenseState(); + const licenseState = licenseStateMock.create(); const router: RouterMock = mockRouter.create(); (verifyApiAccess as jest.Mock).mockImplementation(() => { diff --git a/x-pack/plugins/actions/server/routes/get.ts b/x-pack/plugins/actions/server/routes/get.ts index 836f46bfe55fd..e3c93299614bd 100644 --- a/x-pack/plugins/actions/server/routes/get.ts +++ b/x-pack/plugins/actions/server/routes/get.ts @@ -12,14 +12,13 @@ import { IKibanaResponse, KibanaResponseFactory, } from 'kibana/server'; -import { LicenseState } from '../lib/license_state'; -import { verifyApiAccess } from '../lib/license_api_access'; +import { ILicenseState, verifyApiAccess } from '../lib'; const paramSchema = schema.object({ id: schema.string(), }); -export const getActionRoute = (router: IRouter, licenseState: LicenseState) => { +export const getActionRoute = (router: IRouter, licenseState: ILicenseState) => { router.get( { path: `/api/action/{id}`, diff --git a/x-pack/plugins/actions/server/routes/list_action_types.test.ts b/x-pack/plugins/actions/server/routes/list_action_types.test.ts index e983b8d1f2f84..76fb636a75be7 100644 --- a/x-pack/plugins/actions/server/routes/list_action_types.test.ts +++ b/x-pack/plugins/actions/server/routes/list_action_types.test.ts @@ -6,11 +6,11 @@ import { listActionTypesRoute } from './list_action_types'; import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; -import { mockLicenseState } from '../lib/license_state.mock'; -import { verifyApiAccess } from '../lib/license_api_access'; +import { licenseStateMock } from '../lib/license_state.mock'; +import { verifyApiAccess } from '../lib'; import { mockHandlerArguments } from './_mock_handler_arguments'; -jest.mock('../lib/license_api_access.ts', () => ({ +jest.mock('../lib/verify_api_access.ts', () => ({ verifyApiAccess: jest.fn(), })); @@ -20,7 +20,7 @@ beforeEach(() => { describe('listActionTypesRoute', () => { it('lists action types with proper parameters', async () => { - const licenseState = mockLicenseState(); + const licenseState = licenseStateMock.create(); const router: RouterMock = mockRouter.create(); listActionTypesRoute(router, licenseState); @@ -66,7 +66,7 @@ describe('listActionTypesRoute', () => { }); it('ensures the license allows listing action types', async () => { - const licenseState = mockLicenseState(); + const licenseState = licenseStateMock.create(); const router: RouterMock = mockRouter.create(); listActionTypesRoute(router, licenseState); @@ -104,7 +104,7 @@ describe('listActionTypesRoute', () => { }); it('ensures the license check prevents listing action types', async () => { - const licenseState = mockLicenseState(); + const licenseState = licenseStateMock.create(); const router: RouterMock = mockRouter.create(); (verifyApiAccess as jest.Mock).mockImplementation(() => { diff --git a/x-pack/plugins/actions/server/routes/list_action_types.ts b/x-pack/plugins/actions/server/routes/list_action_types.ts index 46f62e3a9c8bb..6f2b8f86e1fb2 100644 --- a/x-pack/plugins/actions/server/routes/list_action_types.ts +++ b/x-pack/plugins/actions/server/routes/list_action_types.ts @@ -11,10 +11,9 @@ import { IKibanaResponse, KibanaResponseFactory, } from 'kibana/server'; -import { LicenseState } from '../lib/license_state'; -import { verifyApiAccess } from '../lib/license_api_access'; +import { ILicenseState, verifyApiAccess } from '../lib'; -export const listActionTypesRoute = (router: IRouter, licenseState: LicenseState) => { +export const listActionTypesRoute = (router: IRouter, licenseState: ILicenseState) => { router.get( { path: `/api/action/types`, diff --git a/x-pack/plugins/actions/server/routes/update.test.ts b/x-pack/plugins/actions/server/routes/update.test.ts index 1090193761395..161fb4398af1d 100644 --- a/x-pack/plugins/actions/server/routes/update.test.ts +++ b/x-pack/plugins/actions/server/routes/update.test.ts @@ -5,11 +5,11 @@ */ import { updateActionRoute } from './update'; import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; -import { mockLicenseState } from '../lib/license_state.mock'; -import { verifyApiAccess } from '../lib/license_api_access'; +import { licenseStateMock } from '../lib/license_state.mock'; +import { verifyApiAccess, ActionTypeDisabledError } from '../lib'; import { mockHandlerArguments } from './_mock_handler_arguments'; -jest.mock('../lib/license_api_access.ts', () => ({ +jest.mock('../lib/verify_api_access.ts', () => ({ verifyApiAccess: jest.fn(), })); @@ -19,7 +19,7 @@ beforeEach(() => { describe('updateActionRoute', () => { it('updates an action with proper parameters', async () => { - const licenseState = mockLicenseState(); + const licenseState = licenseStateMock.create(); const router: RouterMock = mockRouter.create(); updateActionRoute(router, licenseState); @@ -85,7 +85,7 @@ describe('updateActionRoute', () => { }); it('ensures the license allows deleting actions', async () => { - const licenseState = mockLicenseState(); + const licenseState = licenseStateMock.create(); const router: RouterMock = mockRouter.create(); updateActionRoute(router, licenseState); @@ -124,7 +124,7 @@ describe('updateActionRoute', () => { }); it('ensures the license check prevents deleting actions', async () => { - const licenseState = mockLicenseState(); + const licenseState = licenseStateMock.create(); const router: RouterMock = mockRouter.create(); (verifyApiAccess as jest.Mock).mockImplementation(() => { @@ -165,4 +165,26 @@ describe('updateActionRoute', () => { expect(verifyApiAccess).toHaveBeenCalledWith(licenseState); }); + + it('ensures the action type gets validated for the license', async () => { + const licenseState = licenseStateMock.create(); + const router: RouterMock = mockRouter.create(); + + updateActionRoute(router, licenseState); + + const [, handler] = router.put.mock.calls[0]; + + const actionsClient = { + update: jest.fn().mockRejectedValue(new ActionTypeDisabledError('Fail', 'license_invalid')), + }; + + const [context, req, res] = mockHandlerArguments({ actionsClient }, { params: {}, body: {} }, [ + 'ok', + 'forbidden', + ]); + + await handler(context, req, res); + + expect(res.forbidden).toHaveBeenCalledWith({ body: { message: 'Fail' } }); + }); }); diff --git a/x-pack/plugins/actions/server/routes/update.ts b/x-pack/plugins/actions/server/routes/update.ts index 315695382b2d9..692693f010665 100644 --- a/x-pack/plugins/actions/server/routes/update.ts +++ b/x-pack/plugins/actions/server/routes/update.ts @@ -12,8 +12,7 @@ import { IKibanaResponse, KibanaResponseFactory, } from 'kibana/server'; -import { LicenseState } from '../lib/license_state'; -import { verifyApiAccess } from '../lib/license_api_access'; +import { ILicenseState, verifyApiAccess, isErrorThatHandlesItsOwnResponse } from '../lib'; const paramSchema = schema.object({ id: schema.string(), @@ -25,7 +24,7 @@ const bodySchema = schema.object({ secrets: schema.recordOf(schema.string(), schema.any(), { defaultValue: {} }), }); -export const updateActionRoute = (router: IRouter, licenseState: LicenseState) => { +export const updateActionRoute = (router: IRouter, licenseState: ILicenseState) => { router.put( { path: `/api/action/{id}`, @@ -49,12 +48,20 @@ export const updateActionRoute = (router: IRouter, licenseState: LicenseState) = const actionsClient = context.actions.getActionsClient(); const { id } = req.params; const { name, config, secrets } = req.body; - return res.ok({ - body: await actionsClient.update({ - id, - action: { name, config, secrets }, - }), - }); + + try { + return res.ok({ + body: await actionsClient.update({ + id, + action: { name, config, secrets }, + }), + }); + } catch (e) { + if (isErrorThatHandlesItsOwnResponse(e)) { + return e.sendResponse(res); + } + throw e; + } }) ); }; diff --git a/x-pack/plugins/actions/server/types.ts b/x-pack/plugins/actions/server/types.ts index 635c0829e02c3..999e739e77060 100644 --- a/x-pack/plugins/actions/server/types.ts +++ b/x-pack/plugins/actions/server/types.ts @@ -8,6 +8,7 @@ import { SavedObjectsClientContract, SavedObjectAttributes } from '../../../../s import { ActionTypeRegistry } from './action_type_registry'; import { PluginSetupContract, PluginStartContract } from './plugin'; import { ActionsClient } from './actions_client'; +import { LicenseType } from '../../licensing/common/types'; export type WithoutQueryAndParams = Pick>; export type GetServicesFunction = (request: any) => Services; @@ -84,6 +85,7 @@ export interface ActionType { id: string; name: string; maxAttempts?: number; + minimumLicenseRequired: LicenseType; validate?: { params?: ValidatorType; config?: ValidatorType; diff --git a/x-pack/plugins/alerting/server/plugin.ts b/x-pack/plugins/alerting/server/plugin.ts index 8d54432f7d9c3..58807b42dc278 100644 --- a/x-pack/plugins/alerting/server/plugin.ts +++ b/x-pack/plugins/alerting/server/plugin.ts @@ -206,7 +206,7 @@ export class AlertingPlugin { logger, getServices: this.getServicesFactory(core.savedObjects), spaceIdToNamespace: this.spaceIdToNamespace, - executeAction: plugins.actions.execute, + actionsPlugin: plugins.actions, encryptedSavedObjectsPlugin: plugins.encryptedSavedObjects, getBasePath: this.getBasePath, }); diff --git a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts index 0fb1fa98249ef..5bd8382f0a4b2 100644 --- a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts @@ -7,6 +7,7 @@ import { AlertType } from '../types'; import { createExecutionHandler } from './create_execution_handler'; import { loggingServiceMock } from '../../../../../src/core/server/mocks'; +import { actionsMock } from '../../../actions/server/mocks'; const alertType: AlertType = { id: 'test', @@ -20,7 +21,7 @@ const alertType: AlertType = { }; const createExecutionHandlerParams = { - executeAction: jest.fn(), + actionsPlugin: actionsMock.createStart(), spaceId: 'default', alertId: '1', alertName: 'name-of-alert', @@ -45,9 +46,12 @@ const createExecutionHandlerParams = { ], }; -beforeEach(() => jest.resetAllMocks()); +beforeEach(() => { + jest.resetAllMocks(); + createExecutionHandlerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true); +}); -test('calls executeAction per selected action', async () => { +test('calls actionsPlugin.execute per selected action', async () => { const executionHandler = createExecutionHandler(createExecutionHandlerParams); await executionHandler({ actionGroup: 'default', @@ -55,8 +59,8 @@ test('calls executeAction per selected action', async () => { context: {}, alertInstanceId: '2', }); - expect(createExecutionHandlerParams.executeAction).toHaveBeenCalledTimes(1); - expect(createExecutionHandlerParams.executeAction.mock.calls[0]).toMatchInlineSnapshot(` + expect(createExecutionHandlerParams.actionsPlugin.execute).toHaveBeenCalledTimes(1); + expect(createExecutionHandlerParams.actionsPlugin.execute.mock.calls[0]).toMatchInlineSnapshot(` Array [ Object { "apiKey": "MTIzOmFiYw==", @@ -73,7 +77,46 @@ test('calls executeAction per selected action', async () => { `); }); -test('limits executeAction per action group', async () => { +test(`doesn't call actionsPlugin.execute for disabled actionTypes`, async () => { + // Mock two calls, one for check against actions[0] and the second for actions[1] + createExecutionHandlerParams.actionsPlugin.isActionTypeEnabled.mockReturnValueOnce(false); + createExecutionHandlerParams.actionsPlugin.isActionTypeEnabled.mockReturnValueOnce(true); + const executionHandler = createExecutionHandler({ + ...createExecutionHandlerParams, + actions: [ + ...createExecutionHandlerParams.actions, + { + id: '2', + group: 'default', + actionTypeId: 'test2', + params: { + foo: true, + contextVal: 'My other {{context.value}} goes here', + stateVal: 'My other {{state.value}} goes here', + }, + }, + ], + }); + await executionHandler({ + actionGroup: 'default', + state: {}, + context: {}, + alertInstanceId: '2', + }); + expect(createExecutionHandlerParams.actionsPlugin.execute).toHaveBeenCalledTimes(1); + expect(createExecutionHandlerParams.actionsPlugin.execute).toHaveBeenCalledWith({ + id: '2', + params: { + foo: true, + contextVal: 'My other goes here', + stateVal: 'My other goes here', + }, + spaceId: 'default', + apiKey: createExecutionHandlerParams.apiKey, + }); +}); + +test('limits actionsPlugin.execute per action group', async () => { const executionHandler = createExecutionHandler(createExecutionHandlerParams); await executionHandler({ actionGroup: 'other-group', @@ -81,7 +124,7 @@ test('limits executeAction per action group', async () => { context: {}, alertInstanceId: '2', }); - expect(createExecutionHandlerParams.executeAction).toMatchInlineSnapshot(`[MockFunction]`); + expect(createExecutionHandlerParams.actionsPlugin.execute).not.toHaveBeenCalled(); }); test('context attribute gets parameterized', async () => { @@ -92,8 +135,8 @@ test('context attribute gets parameterized', async () => { state: {}, alertInstanceId: '2', }); - expect(createExecutionHandlerParams.executeAction).toHaveBeenCalledTimes(1); - expect(createExecutionHandlerParams.executeAction.mock.calls[0]).toMatchInlineSnapshot(` + expect(createExecutionHandlerParams.actionsPlugin.execute).toHaveBeenCalledTimes(1); + expect(createExecutionHandlerParams.actionsPlugin.execute.mock.calls[0]).toMatchInlineSnapshot(` Array [ Object { "apiKey": "MTIzOmFiYw==", @@ -118,8 +161,8 @@ test('state attribute gets parameterized', async () => { state: { value: 'state-val' }, alertInstanceId: '2', }); - expect(createExecutionHandlerParams.executeAction).toHaveBeenCalledTimes(1); - expect(createExecutionHandlerParams.executeAction.mock.calls[0]).toMatchInlineSnapshot(` + expect(createExecutionHandlerParams.actionsPlugin.execute).toHaveBeenCalledTimes(1); + expect(createExecutionHandlerParams.actionsPlugin.execute.mock.calls[0]).toMatchInlineSnapshot(` Array [ Object { "apiKey": "MTIzOmFiYw==", diff --git a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts index 5acb171209ea6..5d14f4adc709e 100644 --- a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts +++ b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts @@ -14,7 +14,7 @@ interface CreateExecutionHandlerOptions { alertId: string; alertName: string; tags?: string[]; - executeAction: ActionsPluginStartContract['execute']; + actionsPlugin: ActionsPluginStartContract; actions: AlertAction[]; spaceId: string; apiKey: string | null; @@ -34,7 +34,7 @@ export function createExecutionHandler({ alertId, alertName, tags, - executeAction, + actionsPlugin, actions: alertActions, spaceId, apiKey, @@ -64,12 +64,18 @@ export function createExecutionHandler({ }; }); for (const action of actions) { - await executeAction({ - id: action.id, - params: action.params, - spaceId, - apiKey, - }); + if (actionsPlugin.isActionTypeEnabled(action.actionTypeId)) { + await actionsPlugin.execute({ + id: action.id, + params: action.params, + spaceId, + apiKey, + }); + } else { + logger.warn( + `Alert "${alertId}" skipped scheduling action "${action.id}" because it is disabled` + ); + } } }; } diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts index d1bc0de3ae0e2..5f4669f64f09d 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts @@ -12,6 +12,8 @@ import { TaskRunnerContext } from './task_runner_factory'; import { TaskRunner } from './task_runner'; import { encryptedSavedObjectsMock } from '../../../../plugins/encrypted_saved_objects/server/mocks'; import { savedObjectsClientMock, loggingServiceMock } from '../../../../../src/core/server/mocks'; +import { PluginStartContract as ActionsPluginStart } from '../../../actions/server'; +import { actionsMock } from '../../../actions/server/mocks'; const alertType = { id: 'test', @@ -55,9 +57,11 @@ describe('Task Runner', () => { savedObjectsClient, }; - const taskRunnerFactoryInitializerParams: jest.Mocked = { + const taskRunnerFactoryInitializerParams: jest.Mocked & { + actionsPlugin: jest.Mocked; + } = { getServices: jest.fn().mockReturnValue(services), - executeAction: jest.fn(), + actionsPlugin: actionsMock.createStart(), encryptedSavedObjectsPlugin, logger: loggingServiceMock.create().get(), spaceIdToNamespace: jest.fn().mockReturnValue(undefined), @@ -154,7 +158,8 @@ describe('Task Runner', () => { expect(call.services).toBeTruthy(); }); - test('executeAction is called per alert instance that is scheduled', async () => { + test('actionsPlugin.execute is called per alert instance that is scheduled', async () => { + taskRunnerFactoryInitializerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true); alertType.executor.mockImplementation( ({ services: executorServices }: AlertExecutorOptions) => { executorServices.alertInstanceFactory('1').scheduleActions('default'); @@ -175,8 +180,9 @@ describe('Task Runner', () => { references: [], }); await taskRunner.run(); - expect(taskRunnerFactoryInitializerParams.executeAction).toHaveBeenCalledTimes(1); - expect(taskRunnerFactoryInitializerParams.executeAction.mock.calls[0]).toMatchInlineSnapshot(` + expect(taskRunnerFactoryInitializerParams.actionsPlugin.execute).toHaveBeenCalledTimes(1); + expect(taskRunnerFactoryInitializerParams.actionsPlugin.execute.mock.calls[0]) + .toMatchInlineSnapshot(` Array [ Object { "apiKey": "MTIzOmFiYw==", diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.ts index 5c8acfb58a92a..42768a80a4ccf 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.ts @@ -119,7 +119,7 @@ export class TaskRunner { alertName, tags, logger: this.logger, - executeAction: this.context.executeAction, + actionsPlugin: this.context.actionsPlugin, apiKey, actions: actionsWithIds, spaceId, diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner_factory.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner_factory.test.ts index f885b0bdbd046..fc34cacba2818 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner_factory.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner_factory.test.ts @@ -9,6 +9,7 @@ import { ConcreteTaskInstance, TaskStatus } from '../../../../plugins/task_manag import { TaskRunnerContext, TaskRunnerFactory } from './task_runner_factory'; import { encryptedSavedObjectsMock } from '../../../../plugins/encrypted_saved_objects/server/mocks'; import { savedObjectsClientMock, loggingServiceMock } from '../../../../../src/core/server/mocks'; +import { actionsMock } from '../../../actions/server/mocks'; const alertType = { id: 'test', @@ -56,7 +57,7 @@ describe('Task Runner Factory', () => { const taskRunnerFactoryInitializerParams: jest.Mocked = { getServices: jest.fn().mockReturnValue(services), - executeAction: jest.fn(), + actionsPlugin: actionsMock.createStart(), encryptedSavedObjectsPlugin, logger: loggingServiceMock.create().get(), spaceIdToNamespace: jest.fn().mockReturnValue(undefined), diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner_factory.ts b/x-pack/plugins/alerting/server/task_runner/task_runner_factory.ts index c598b0f52f197..3bad4e475ff49 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner_factory.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner_factory.ts @@ -18,7 +18,7 @@ import { TaskRunner } from './task_runner'; export interface TaskRunnerContext { logger: Logger; getServices: GetServicesFunction; - executeAction: ActionsPluginStartContract['execute']; + actionsPlugin: ActionsPluginStartContract; encryptedSavedObjectsPlugin: EncryptedSavedObjectsPluginStart; spaceIdToNamespace: SpaceIdToNamespaceFunction; getBasePath: GetBasePathFunction; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api.test.ts index 62e7b1cf022bb..ee68b7e269c34 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api.test.ts @@ -25,6 +25,9 @@ describe('loadActionTypes', () => { id: 'test', name: 'Test', enabled: true, + enabledInConfig: true, + enabledInLicense: true, + minimumLicenseRequired: 'basic', }, ]; http.get.mockResolvedValueOnce(resolvedValue); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_type_compare.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_type_compare.test.ts new file mode 100644 index 0000000000000..9ce50cf47560a --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_type_compare.test.ts @@ -0,0 +1,74 @@ +/* + * 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 { ActionType } from '../../types'; +import { actionTypeCompare } from './action_type_compare'; + +test('should sort enabled action types first', async () => { + const actionTypes: ActionType[] = [ + { + id: '1', + minimumLicenseRequired: 'basic', + name: 'first', + enabled: true, + enabledInConfig: true, + enabledInLicense: true, + }, + { + id: '2', + minimumLicenseRequired: 'gold', + name: 'second', + enabled: false, + enabledInConfig: true, + enabledInLicense: false, + }, + { + id: '3', + minimumLicenseRequired: 'basic', + name: 'third', + enabled: true, + enabledInConfig: true, + enabledInLicense: true, + }, + ]; + const result = [...actionTypes].sort(actionTypeCompare); + expect(result[0]).toEqual(actionTypes[0]); + expect(result[1]).toEqual(actionTypes[2]); + expect(result[2]).toEqual(actionTypes[1]); +}); + +test('should sort by name when all enabled', async () => { + const actionTypes: ActionType[] = [ + { + id: '1', + minimumLicenseRequired: 'basic', + name: 'third', + enabled: true, + enabledInConfig: true, + enabledInLicense: true, + }, + { + id: '2', + minimumLicenseRequired: 'basic', + name: 'first', + enabled: true, + enabledInConfig: true, + enabledInLicense: true, + }, + { + id: '3', + minimumLicenseRequired: 'basic', + name: 'second', + enabled: true, + enabledInConfig: true, + enabledInLicense: true, + }, + ]; + const result = [...actionTypes].sort(actionTypeCompare); + expect(result[0]).toEqual(actionTypes[1]); + expect(result[1]).toEqual(actionTypes[2]); + expect(result[2]).toEqual(actionTypes[0]); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_type_compare.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_type_compare.ts new file mode 100644 index 0000000000000..d18cb21b3a0fe --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_type_compare.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ActionType } from '../../types'; + +export function actionTypeCompare(a: ActionType, b: ActionType) { + if (a.enabled === true && b.enabled === false) { + return -1; + } + if (a.enabled === false && b.enabled === true) { + return 1; + } + return a.name.localeCompare(b.name); +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/check_action_type_enabled.scss b/x-pack/plugins/triggers_actions_ui/public/application/lib/check_action_type_enabled.scss new file mode 100644 index 0000000000000..32ab1bd7b1821 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/check_action_type_enabled.scss @@ -0,0 +1,9 @@ +.actCheckActionTypeEnabled__disabledActionWarningCard { + background-color: $euiColorLightestShade; +} + +.actAccordionActionForm { + .euiCard { + box-shadow: none; + } +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/check_action_type_enabled.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/lib/check_action_type_enabled.test.tsx new file mode 100644 index 0000000000000..eb51bb8ac5098 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/check_action_type_enabled.test.tsx @@ -0,0 +1,88 @@ +/* + * 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 { ActionType } from '../../types'; +import { checkActionTypeEnabled } from './check_action_type_enabled'; + +test(`returns isEnabled:true when action type isn't provided`, async () => { + expect(checkActionTypeEnabled()).toMatchInlineSnapshot(` + Object { + "isEnabled": true, + } + `); +}); + +test('returns isEnabled:true when action type is enabled', async () => { + const actionType: ActionType = { + id: '1', + minimumLicenseRequired: 'basic', + name: 'my action', + enabled: true, + enabledInConfig: true, + enabledInLicense: true, + }; + expect(checkActionTypeEnabled(actionType)).toMatchInlineSnapshot(` + Object { + "isEnabled": true, + } + `); +}); + +test('returns isEnabled:false when action type is disabled by license', async () => { + const actionType: ActionType = { + id: '1', + minimumLicenseRequired: 'basic', + name: 'my action', + enabled: false, + enabledInConfig: true, + enabledInLicense: false, + }; + expect(checkActionTypeEnabled(actionType)).toMatchInlineSnapshot(` + Object { + "isEnabled": false, + "message": "This connector is disabled because it requires a basic license.", + "messageCard": + + + + , + } + `); +}); + +test('returns isEnabled:false when action type is disabled by config', async () => { + const actionType: ActionType = { + id: '1', + minimumLicenseRequired: 'basic', + name: 'my action', + enabled: false, + enabledInConfig: false, + enabledInLicense: true, + }; + expect(checkActionTypeEnabled(actionType)).toMatchInlineSnapshot(` + Object { + "isEnabled": false, + "message": "This connector is disabled by the Kibana configuration.", + "messageCard": , + } + `); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/check_action_type_enabled.tsx b/x-pack/plugins/triggers_actions_ui/public/application/lib/check_action_type_enabled.tsx new file mode 100644 index 0000000000000..7691c3741468c --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/check_action_type_enabled.tsx @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiCard, EuiLink } from '@elastic/eui'; +import { ActionType } from '../../types'; +import { VIEW_LICENSE_OPTIONS_LINK } from '../../common/constants'; +import './check_action_type_enabled.scss'; + +export interface IsEnabledResult { + isEnabled: true; +} +export interface IsDisabledResult { + isEnabled: false; + message: string; + messageCard: JSX.Element; +} + +export function checkActionTypeEnabled( + actionType?: ActionType +): IsEnabledResult | IsDisabledResult { + if (actionType?.enabledInLicense === false) { + return { + isEnabled: false, + message: i18n.translate( + 'xpack.triggersActionsUI.checkActionTypeEnabled.actionTypeDisabledByLicenseMessage', + { + defaultMessage: + 'This connector is disabled because it requires a {minimumLicenseRequired} license.', + values: { + minimumLicenseRequired: actionType.minimumLicenseRequired, + }, + } + ), + messageCard: ( + + + + } + /> + ), + }; + } + + if (actionType?.enabledInConfig === false) { + return { + isEnabled: false, + message: i18n.translate( + 'xpack.triggersActionsUI.checkActionTypeEnabled.actionTypeDisabledByConfigMessage', + { defaultMessage: 'This connector is disabled by the Kibana configuration.' } + ), + messageCard: ( + + ), + }; + } + + return { isEnabled: true }; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx index caed0caefe109..89d37c4d00a11 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx @@ -39,6 +39,36 @@ describe('action_form', () => { actionParamsFields: null, }; + const disabledByConfigActionType = { + id: 'disabled-by-config', + iconClass: 'test', + selectMessage: 'test', + validateConnector: (): ValidationResult => { + return { errors: {} }; + }, + validateParams: (): ValidationResult => { + const validationResult = { errors: {} }; + return validationResult; + }, + actionConnectorFields: null, + actionParamsFields: null, + }; + + const disabledByLicenseActionType = { + id: 'disabled-by-license', + iconClass: 'test', + selectMessage: 'test', + validateConnector: (): ValidationResult => { + return { errors: {} }; + }, + validateParams: (): ValidationResult => { + const validationResult = { errors: {} }; + return validationResult; + }, + actionConnectorFields: null, + actionParamsFields: null, + }; + describe('action_form in alert', () => { let wrapper: ReactWrapper; @@ -49,7 +79,11 @@ describe('action_form', () => { http: mockes.http, actionTypeRegistry: actionTypeRegistry as any, }; - actionTypeRegistry.list.mockReturnValue([actionType]); + actionTypeRegistry.list.mockReturnValue([ + actionType, + disabledByConfigActionType, + disabledByLicenseActionType, + ]); actionTypeRegistry.has.mockReturnValue(true); const initialAlert = ({ @@ -92,8 +126,38 @@ describe('action_form', () => { actionTypeRegistry={deps!.actionTypeRegistry} defaultActionMessage={'Alert [{{ctx.metadata.name}}] has exceeded the threshold'} actionTypes={[ - { id: actionType.id, name: 'Test', enabled: true }, - { id: '.index', name: 'Index', enabled: true }, + { + id: actionType.id, + name: 'Test', + enabled: true, + enabledInConfig: true, + enabledInLicense: true, + minimumLicenseRequired: 'basic', + }, + { + id: '.index', + name: 'Index', + enabled: true, + enabledInConfig: true, + enabledInLicense: true, + minimumLicenseRequired: 'basic', + }, + { + id: 'disabled-by-config', + name: 'Disabled by config', + enabled: false, + enabledInConfig: false, + enabledInLicense: true, + minimumLicenseRequired: 'gold', + }, + { + id: 'disabled-by-license', + name: 'Disabled by license', + enabled: false, + enabledInConfig: true, + enabledInLicense: false, + minimumLicenseRequired: 'gold', + }, ]} toastNotifications={deps!.toastNotifications} /> @@ -112,6 +176,32 @@ describe('action_form', () => { `[data-test-subj="${actionType.id}-ActionTypeSelectOption"]` ); expect(actionOption.exists()).toBeTruthy(); + expect( + wrapper + .find(`EuiToolTip [data-test-subj="${actionType.id}-ActionTypeSelectOption"]`) + .exists() + ).toBeFalsy(); + }); + + it(`doesn't render action types disabled by config`, async () => { + await setup(); + const actionOption = wrapper.find( + `[data-test-subj="disabled-by-config-ActionTypeSelectOption"]` + ); + expect(actionOption.exists()).toBeFalsy(); + }); + + it('renders action types disabled by license', async () => { + await setup(); + const actionOption = wrapper.find( + `[data-test-subj="disabled-by-license-ActionTypeSelectOption"]` + ); + expect(actionOption.exists()).toBeTruthy(); + expect( + wrapper + .find('EuiToolTip [data-test-subj="disabled-by-license-ActionTypeSelectOption"]') + .exists() + ).toBeTruthy(); }); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx index 64be161fc90b3..18bc6ad8810a0 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx @@ -21,6 +21,9 @@ import { EuiButtonIcon, EuiEmptyPrompt, EuiButtonEmpty, + EuiToolTip, + EuiIconTip, + EuiLink, } from '@elastic/eui'; import { HttpSetup, ToastsApi } from 'kibana/public'; import { loadActionTypes, loadAllActions } from '../../lib/action_connector_api'; @@ -35,6 +38,9 @@ import { import { SectionLoading } from '../../components/section_loading'; import { ConnectorAddModal } from './connector_add_modal'; import { TypeRegistry } from '../../type_registry'; +import { actionTypeCompare } from '../../lib/action_type_compare'; +import { checkActionTypeEnabled } from '../../lib/check_action_type_enabled'; +import { VIEW_LICENSE_OPTIONS_LINK } from '../../../common/constants'; interface ActionAccordionFormProps { actions: AlertAction[]; @@ -51,6 +57,7 @@ interface ActionAccordionFormProps { actionTypes?: ActionType[]; messageVariables?: string[]; defaultActionMessage?: string; + setHasActionsDisabled?: (value: boolean) => void; } interface ActiveActionConnectorState { @@ -70,6 +77,7 @@ export const ActionForm = ({ messageVariables, defaultActionMessage, toastNotifications, + setHasActionsDisabled, }: ActionAccordionFormProps) => { const [addModalVisible, setAddModalVisibility] = useState(false); const [activeActionItem, setActiveActionItem] = useState( @@ -91,6 +99,10 @@ export const ActionForm = ({ index[actionTypeItem.id] = actionTypeItem; } setActionTypesIndex(index); + const hasActionsDisabled = actions.some(action => !index[action.actionTypeId].enabled); + if (setHasActionsDisabled) { + setHasActionsDisabled(hasActionsDisabled); + } } catch (e) { if (toastNotifications) { toastNotifications.addDanger({ @@ -179,60 +191,12 @@ export const ActionForm = ({ const ParamsFieldsComponent = actionTypeRegistered.actionParamsFields; const actionParamsErrors: { errors: IErrorObject } = Object.keys(actionsErrors).length > 0 ? actionsErrors[actionItem.id] : { errors: {} }; + const checkEnabledResult = checkActionTypeEnabled( + actionTypesIndex && actionTypesIndex[actionConnector.actionTypeId] + ); - return ( - - - - - - -
- -
-
-
- - } - extraAction={ - { - const updatedActions = actions.filter( - (item: AlertAction) => item.id !== actionItem.id - ); - setAlertProperty(updatedActions); - setIsAddActionPanelOpen( - updatedActions.filter((item: AlertAction) => item.id !== actionItem.id).length === 0 - ); - setActiveActionItem(undefined); - }} - /> - } - paddingSize="l" - > + const accordionContent = checkEnabledResult.isEnabled ? ( + ) : null} + + ) : ( + checkEnabledResult.messageCard + ); + + return ( + + + + + + +
+ + + + + + {checkEnabledResult.isEnabled === false && ( + + + + )} + + +
+
+
+ + } + extraAction={ + { + const updatedActions = actions.filter( + (item: AlertAction) => item.id !== actionItem.id + ); + setAlertProperty(updatedActions); + setIsAddActionPanelOpen( + updatedActions.filter((item: AlertAction) => item.id !== actionItem.id).length === 0 + ); + setActiveActionItem(undefined); + }} + /> + } + paddingSize="l" + > + {accordionContent}
); }; @@ -302,8 +346,8 @@ export const ActionForm = ({ initialIsOpen={true} key={index} id={index.toString()} - className="euiAccordionForm" - buttonContentClassName="euiAccordionForm__button" + className="actAccordionActionForm" + buttonContentClassName="actAccordionActionForm__button" data-test-subj={`alertActionAccordion-${defaultActionGroupId}`} buttonContent={ @@ -329,7 +373,7 @@ export const ActionForm = ({ actionTypesIndex[item.id] && actionTypesIndex[item.id].enabledInConfig === true + ) + .sort((a, b) => actionTypeCompare(actionTypesIndex[a.id], actionTypesIndex[b.id])) + .map(function(item, index) { + const actionType = actionTypesIndex[item.id]; + const checkEnabledResult = checkActionTypeEnabled(actionTypesIndex[item.id]); + if (!actionType.enabledInLicense) { + hasDisabledByLicenseActionTypes = true; + } + + const keyPadItem = ( addActionType(item)} > - ) : null; - }) - : null; + ); + + return ( + + {checkEnabledResult.isEnabled && keyPadItem} + {checkEnabledResult.isEnabled === false && ( + + {keyPadItem} + + )} + + ); + }); + } return ( @@ -467,14 +537,36 @@ export const ActionForm = ({ ) : null} {isAddActionPanelOpen ? ( - -
- -
-
+ + + +
+ +
+
+
+ {hasDisabledByLicenseActionTypes && ( + + +
+ + + +
+
+
+ )} +
{isLoadingActionTypes ? ( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_menu.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_menu.test.tsx index 4f098165033e7..84d5269337b9e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_menu.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_menu.test.tsx @@ -77,6 +77,9 @@ describe('connector_add_flyout', () => { id: actionType.id, enabled: true, name: 'Test', + enabledInConfig: true, + enabledInLicense: true, + minimumLicenseRequired: 'basic', }, ]} /> @@ -85,4 +88,107 @@ describe('connector_add_flyout', () => { expect(wrapper.find('[data-test-subj="my-action-type-card"]').exists()).toBeTruthy(); }); + + it(`doesn't renders action types that are disabled via config`, () => { + const onActionTypeChange = jest.fn(); + const actionType = { + id: 'my-action-type', + iconClass: 'test', + selectMessage: 'test', + validateConnector: (): ValidationResult => { + return { errors: {} }; + }, + validateParams: (): ValidationResult => { + const validationResult = { errors: {} }; + return validationResult; + }, + actionConnectorFields: null, + actionParamsFields: null, + }; + actionTypeRegistry.get.mockReturnValueOnce(actionType); + + const wrapper = mountWithIntl( + { + return new Promise(() => {}); + }, + }} + > + + + ); + + expect(wrapper.find('[data-test-subj="my-action-type-card"]').exists()).toBeFalsy(); + }); + + it(`renders action types as disabled when disabled by license`, () => { + const onActionTypeChange = jest.fn(); + const actionType = { + id: 'my-action-type', + iconClass: 'test', + selectMessage: 'test', + validateConnector: (): ValidationResult => { + return { errors: {} }; + }, + validateParams: (): ValidationResult => { + const validationResult = { errors: {} }; + return validationResult; + }, + actionConnectorFields: null, + actionParamsFields: null, + }; + actionTypeRegistry.get.mockReturnValueOnce(actionType); + + const wrapper = mountWithIntl( + { + return new Promise(() => {}); + }, + }} + > + + + ); + + const element = wrapper.find('[data-test-subj="my-action-type-card"]'); + expect(element.exists()).toBeTruthy(); + expect(element.first().prop('betaBadgeLabel')).toEqual('Upgrade'); + expect(element.first().prop('betaBadgeTooltipContent')).toEqual( + 'This connector is disabled because it requires a gold license.' + ); + }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_menu.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_menu.tsx index a63665a68fb6b..2dd5e413faf9c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_menu.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_menu.tsx @@ -4,18 +4,25 @@ * you may not use this file except in compliance with the Elastic License. */ import React, { useEffect, useState } from 'react'; -import { EuiFlexItem, EuiCard, EuiIcon, EuiFlexGrid } from '@elastic/eui'; +import { EuiFlexItem, EuiCard, EuiIcon, EuiFlexGrid, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { ActionType, ActionTypeIndex } from '../../../types'; import { loadActionTypes } from '../../lib/action_connector_api'; import { useActionsConnectorsContext } from '../../context/actions_connectors_context'; +import { actionTypeCompare } from '../../lib/action_type_compare'; +import { checkActionTypeEnabled } from '../../lib/check_action_type_enabled'; interface Props { onActionTypeChange: (actionType: ActionType) => void; actionTypes?: ActionType[]; + setHasActionsDisabledByLicense?: (value: boolean) => void; } -export const ActionTypeMenu = ({ onActionTypeChange, actionTypes }: Props) => { +export const ActionTypeMenu = ({ + onActionTypeChange, + actionTypes, + setHasActionsDisabledByLicense, +}: Props) => { const { http, toastNotifications, actionTypeRegistry } = useActionsConnectorsContext(); const [actionTypesIndex, setActionTypesIndex] = useState(undefined); @@ -28,6 +35,12 @@ export const ActionTypeMenu = ({ onActionTypeChange, actionTypes }: Props) => { index[actionTypeItem.id] = actionTypeItem; } setActionTypesIndex(index); + if (setHasActionsDisabledByLicense) { + const hasActionsDisabledByLicense = availableActionTypes.some( + action => !index[action.id].enabledInLicense + ); + setHasActionsDisabledByLicense(hasActionsDisabledByLicense); + } } catch (e) { if (toastNotifications) { toastNotifications.addDanger({ @@ -43,33 +56,54 @@ export const ActionTypeMenu = ({ onActionTypeChange, actionTypes }: Props) => { }, []); const registeredActionTypes = Object.entries(actionTypesIndex ?? []) - .filter(([index]) => actionTypeRegistry.has(index)) - .map(([index, actionType]) => { - const actionTypeModel = actionTypeRegistry.get(index); + .filter(([id, details]) => actionTypeRegistry.has(id) && details.enabledInConfig === true) + .map(([id, actionType]) => { + const actionTypeModel = actionTypeRegistry.get(id); return { iconClass: actionTypeModel ? actionTypeModel.iconClass : '', selectMessage: actionTypeModel ? actionTypeModel.selectMessage : '', actionType, name: actionType.name, - typeName: index.replace('.', ''), + typeName: id.replace('.', ''), }; }); const cardNodes = registeredActionTypes - .sort((a, b) => a.name.localeCompare(b.name)) + .sort((a, b) => actionTypeCompare(a.actionType, b.actionType)) .map((item, index) => { - return ( - - } - title={item.name} - description={item.selectMessage} - onClick={() => onActionTypeChange(item.actionType)} - /> - + const checkEnabledResult = checkActionTypeEnabled(item.actionType); + const card = ( + } + title={item.name} + description={item.selectMessage} + isDisabled={!checkEnabledResult.isEnabled} + onClick={() => onActionTypeChange(item.actionType)} + betaBadgeLabel={ + checkEnabledResult.isEnabled + ? undefined + : i18n.translate( + 'xpack.triggersActionsUI.sections.actionsConnectorsList.upgradeBadge', + { defaultMessage: 'Upgrade' } + ) + } + betaBadgeTooltipContent={ + checkEnabledResult.isEnabled ? undefined : checkEnabledResult.message + } + /> ); + + return {card}; }); - return {cardNodes}; + return ( +
+ + + {cardNodes} + +
+ ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.test.tsx index cf0edbe422495..c25cae832006a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.test.tsx @@ -79,6 +79,9 @@ describe('connector_add_flyout', () => { id: actionType.id, enabled: true, name: 'Test', + enabledInConfig: true, + enabledInLicense: true, + minimumLicenseRequired: 'basic', }, ]} /> diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx index 9aea2419ec619..665eeca43acb4 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx @@ -18,6 +18,9 @@ import { EuiButton, EuiFlyoutBody, EuiBetaBadge, + EuiCallOut, + EuiLink, + EuiSpacer, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { ActionTypeMenu } from './action_type_menu'; @@ -27,6 +30,7 @@ import { connectorReducer } from './connector_reducer'; import { hasSaveActionsCapability } from '../../lib/capabilities'; import { createActionConnector } from '../../lib/action_connector_api'; import { useActionsConnectorsContext } from '../../context/actions_connectors_context'; +import { VIEW_LICENSE_OPTIONS_LINK } from '../../../common/constants'; export interface ConnectorAddFlyoutProps { addFlyoutVisible: boolean; @@ -48,6 +52,7 @@ export const ConnectorAddFlyout = ({ reloadConnectors, } = useActionsConnectorsContext(); const [actionType, setActionType] = useState(undefined); + const [hasActionsDisabledByLicense, setHasActionsDisabledByLicense] = useState(false); // hooks const initialConnector = { @@ -86,7 +91,11 @@ export const ConnectorAddFlyout = ({ let actionTypeModel; if (!actionType) { currentForm = ( - + ); } else { actionTypeModel = actionTypeRegistry.get(actionType.id); @@ -204,7 +213,11 @@ export const ConnectorAddFlyout = ({
- {currentForm} + + {currentForm} + @@ -252,3 +265,24 @@ export const ConnectorAddFlyout = ({ ); }; + +const upgradeYourLicenseCallOut = ( + + + + + + + +); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.test.tsx index 31d801bb340f3..d2e3739c1cd22 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.test.tsx @@ -8,7 +8,7 @@ import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { coreMock } from '../../../../../../../src/core/public/mocks'; import { ConnectorAddModal } from './connector_add_modal'; import { actionTypeRegistryMock } from '../../action_type_registry.mock'; -import { ValidationResult } from '../../../types'; +import { ValidationResult, ActionType } from '../../../types'; import { ActionsConnectorsContextValue } from '../../context/actions_connectors_context'; const actionTypeRegistry = actionTypeRegistryMock.create(); @@ -54,10 +54,13 @@ describe('connector_add_modal', () => { actionTypeRegistry.get.mockReturnValueOnce(actionTypeModel); actionTypeRegistry.has.mockReturnValue(true); - const actionType = { + const actionType: ActionType = { id: 'my-action-type', name: 'test', enabled: true, + enabledInConfig: true, + enabledInLicense: true, + minimumLicenseRequired: 'basic', }; const wrapper = deps diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.scss b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.scss index 7a824aaeaa8d8..3d65b8a799b1b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.scss +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.scss @@ -1,3 +1,15 @@ .actConnectorsList__logo + .actConnectorsList__logo { margin-left: $euiSize; } + +.actConnectorsList__tableRowDisabled { + background-color: $euiColorLightestShade; + + .actConnectorsList__tableCellDisabled { + color: $euiColorDarkShade; + } + + .euiLink + .euiIcon { + margin-left: $euiSizeXS; + } +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx index 9187836d52462..9331fe1704694 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx @@ -136,10 +136,12 @@ describe('actions_connectors_list component with items', () => { { id: 'test', name: 'Test', + enabled: true, }, { id: 'test2', name: 'Test2', + enabled: true, }, ]); @@ -375,6 +377,117 @@ describe('actions_connectors_list with show only capability', () => { }); }); +describe('actions_connectors_list component with disabled items', () => { + let wrapper: ReactWrapper; + + beforeAll(async () => { + const { loadAllActions, loadActionTypes } = jest.requireMock( + '../../../lib/action_connector_api' + ); + loadAllActions.mockResolvedValueOnce({ + page: 1, + perPage: 10000, + total: 2, + data: [ + { + id: '1', + actionTypeId: 'test', + description: 'My test', + referencedByCount: 1, + config: {}, + }, + { + id: '2', + actionTypeId: 'test2', + description: 'My test 2', + referencedByCount: 1, + config: {}, + }, + ], + }); + loadActionTypes.mockResolvedValueOnce([ + { + id: 'test', + name: 'Test', + enabled: false, + enabledInConfig: false, + enabledInLicense: true, + }, + { + id: 'test2', + name: 'Test2', + enabled: false, + enabledInConfig: true, + enabledInLicense: false, + }, + ]); + + const mockes = coreMock.createSetup(); + const [ + { + chrome, + docLinks, + application: { capabilities, navigateToApp }, + }, + ] = await mockes.getStartServices(); + const deps = { + chrome, + docLinks, + dataPlugin: dataPluginMock.createStartContract(), + charts: chartPluginMock.createStartContract(), + toastNotifications: mockes.notifications.toasts, + injectedMetadata: mockes.injectedMetadata, + http: mockes.http, + uiSettings: mockes.uiSettings, + navigateToApp, + capabilities: { + ...capabilities, + siem: { + 'actions:show': true, + 'actions:save': true, + 'actions:delete': true, + }, + }, + setBreadcrumbs: jest.fn(), + actionTypeRegistry: { + get() { + return null; + }, + } as any, + alertTypeRegistry: {} as any, + }; + + await act(async () => { + wrapper = mountWithIntl( + + + + ); + }); + + await waitForRender(wrapper); + + expect(loadAllActions).toHaveBeenCalled(); + }); + + it('renders table of connectors', () => { + expect(wrapper.find('EuiInMemoryTable')).toHaveLength(1); + expect(wrapper.find('EuiTableRow')).toHaveLength(2); + expect( + wrapper + .find('EuiTableRow') + .at(0) + .prop('className') + ).toEqual('actConnectorsList__tableRowDisabled'); + expect( + wrapper + .find('EuiTableRow') + .at(1) + .prop('className') + ).toEqual('actConnectorsList__tableRowDisabled'); + }); +}); + async function waitForRender(wrapper: ReactWrapper) { await Promise.resolve(); await Promise.resolve(); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx index 9444b31a8b78f..c023f9087d70e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx @@ -15,17 +15,19 @@ import { EuiTitle, EuiLink, EuiLoadingSpinner, + EuiIconTip, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { useAppDependencies } from '../../../app_context'; import { loadAllActions, loadActionTypes } from '../../../lib/action_connector_api'; -import { ActionConnector, ActionConnectorTableItem, ActionTypeIndex } from '../../../../types'; import { ConnectorAddFlyout, ConnectorEditFlyout } from '../../action_connector_form'; import { hasDeleteActionsCapability, hasSaveActionsCapability } from '../../../lib/capabilities'; import { DeleteConnectorsModal } from '../../../components/delete_connectors_modal'; import { ActionsConnectorsContextProvider } from '../../../context/actions_connectors_context'; +import { checkActionTypeEnabled } from '../../../lib/check_action_type_enabled'; import './actions_connectors_list.scss'; +import { ActionConnector, ActionConnectorTableItem, ActionTypeIndex } from '../../../../types'; export const ActionsConnectorsList: React.FunctionComponent = () => { const { http, toastNotifications, capabilities, actionTypeRegistry } = useAppDependencies(); @@ -139,11 +141,33 @@ export const ActionsConnectorsList: React.FunctionComponent = () => { sortable: false, truncateText: true, render: (value: string, item: ActionConnectorTableItem) => { - return ( - editItem(item)} key={item.id}> + const checkEnabledResult = checkActionTypeEnabled( + actionTypesIndex && actionTypesIndex[item.actionTypeId] + ); + + const link = ( + editItem(item)} + key={item.id} + disabled={actionTypesIndex ? !actionTypesIndex[item.actionTypeId].enabled : true} + > {value} ); + + return checkEnabledResult.isEnabled ? ( + link + ) : ( + + {link} + + + ); }, }, { @@ -211,11 +235,19 @@ export const ActionsConnectorsList: React.FunctionComponent = () => { sorting={true} itemId="id" columns={actionsTableColumns} - rowProps={() => ({ + rowProps={(item: ActionConnectorTableItem) => ({ + className: + !actionTypesIndex || !actionTypesIndex[item.actionTypeId].enabled + ? 'actConnectorsList__tableRowDisabled' + : '', 'data-test-subj': 'connectors-row', })} - cellProps={() => ({ + cellProps={(item: ActionConnectorTableItem) => ({ 'data-test-subj': 'cell', + className: + !actionTypesIndex || !actionTypesIndex[item.actionTypeId].enabled + ? 'actConnectorsList__tableCellDisabled' + : '', })} data-test-subj="actionsTable" pagination={true} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx index 92b3e4eb9679f..f025b0396f04d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx @@ -124,6 +124,9 @@ describe('alert_details', () => { id: '.server-log', name: 'Server log', enabled: true, + enabledInConfig: true, + enabledInLicense: true, + minimumLicenseRequired: 'basic', }, ]; @@ -173,11 +176,17 @@ describe('alert_details', () => { id: '.server-log', name: 'Server log', enabled: true, + enabledInConfig: true, + enabledInLicense: true, + minimumLicenseRequired: 'basic', }, { id: '.email', name: 'Send email', enabled: true, + enabledInConfig: true, + enabledInLicense: true, + minimumLicenseRequired: 'basic', }, ]; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx index ac3951cfa98de..cd368193e5fa4 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback, useReducer, useState } from 'react'; +import React, { Fragment, useCallback, useReducer, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiTitle, @@ -17,6 +17,8 @@ import { EuiFlyoutBody, EuiPortal, EuiBetaBadge, + EuiCallOut, + EuiSpacer, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { useAlertsContext } from '../../context/alerts_context'; @@ -38,6 +40,7 @@ export const AlertEdit = ({ }: AlertEditProps) => { const [{ alert }, dispatch] = useReducer(alertReducer, { alert: initialAlert }); const [isSaving, setIsSaving] = useState(false); + const [hasActionsDisabled, setHasActionsDisabled] = useState(false); const { reloadAlerts, @@ -141,7 +144,27 @@ export const AlertEdit = ({ - + {hasActionsDisabled && ( + + + + + )} + diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx index 8382cbe825da3..c6346ba002a7f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx @@ -74,9 +74,16 @@ interface AlertFormProps { dispatch: React.Dispatch; errors: IErrorObject; canChangeTrigger?: boolean; // to hide Change trigger button + setHasActionsDisabled?: (value: boolean) => void; } -export const AlertForm = ({ alert, canChangeTrigger = true, dispatch, errors }: AlertFormProps) => { +export const AlertForm = ({ + alert, + canChangeTrigger = true, + dispatch, + errors, + setHasActionsDisabled, +}: AlertFormProps) => { const alertsContext = useAlertsContext(); const { http, toastNotifications, alertTypeRegistry, actionTypeRegistry } = alertsContext; @@ -218,6 +225,7 @@ export const AlertForm = ({ alert, canChangeTrigger = true, dispatch, errors }: {defaultActionGroupId ? ( av.name) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx index 4bcfef78abd71..a69e276a5fed5 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx @@ -216,31 +216,25 @@ export const AlertsList: React.FunctionComponent = () => { 'data-test-subj': 'alertsTableCell-interval', }, { - field: '', name: '', width: '50px', - actions: canSave - ? [ - { - render: (item: AlertTableItem) => { - return alertTypeRegistry.has(item.alertTypeId) ? ( - editItem(item)} - > - - - ) : ( - <> - ); - }, - }, - ] - : [], + render(item: AlertTableItem) { + if (!canSave || !alertTypeRegistry.has(item.alertTypeId)) { + return; + } + return ( + editItem(item)} + > + + + ); + }, }, { name: '', diff --git a/x-pack/plugins/triggers_actions_ui/public/common/constants/index.ts b/x-pack/plugins/triggers_actions_ui/public/common/constants/index.ts index 816dc894ab9ec..a2a1657a1f4cc 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/constants/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/common/constants/index.ts @@ -7,3 +7,5 @@ export { COMPARATORS, builtInComparators } from './comparators'; export { AGGREGATION_TYPES, builtInAggregationTypes } from './aggregation_types'; export { builtInGroupByTypes } from './group_by_types'; + +export const VIEW_LICENSE_OPTIONS_LINK = 'https://www.elastic.co/subscriptions'; diff --git a/x-pack/plugins/triggers_actions_ui/public/index.ts b/x-pack/plugins/triggers_actions_ui/public/index.ts index 668a8802d1461..342401c4778d8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/index.ts @@ -11,7 +11,7 @@ export { AlertsContextProvider } from './application/context/alerts_context'; export { ActionsConnectorsContextProvider } from './application/context/actions_connectors_context'; export { AlertAdd } from './application/sections/alert_form'; export { ActionForm } from './application/sections/action_connector_form'; -export { AlertAction, Alert, AlertTypeModel } from './types'; +export { AlertAction, Alert, AlertTypeModel, ActionType } from './types'; export { ConnectorAddFlyout, ConnectorEditFlyout, diff --git a/x-pack/scripts/functional_tests.js b/x-pack/scripts/functional_tests.js index c1f8047c8a5cc..06ee0b91c8a5d 100644 --- a/x-pack/scripts/functional_tests.js +++ b/x-pack/scripts/functional_tests.js @@ -13,6 +13,7 @@ const onlyNotInCoverageTests = [ require.resolve('../test/functional/config_security_basic.js'), require.resolve('../test/api_integration/config_security_basic.js'), require.resolve('../test/api_integration/config.js'), + require.resolve('../test/alerting_api_integration/basic/config.ts'), require.resolve('../test/alerting_api_integration/spaces_only/config.ts'), require.resolve('../test/alerting_api_integration/security_and_spaces/config.ts'), require.resolve('../test/detection_engine_api_integration/security_and_spaces/config.ts'), diff --git a/x-pack/test/alerting_api_integration/basic/config.ts b/x-pack/test/alerting_api_integration/basic/config.ts new file mode 100644 index 0000000000000..f9c248ec3d56f --- /dev/null +++ b/x-pack/test/alerting_api_integration/basic/config.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createTestConfig } from '../common/config'; + +// eslint-disable-next-line import/no-default-export +export default createTestConfig('basic', { + disabledPlugins: [], + license: 'basic', + ssl: true, +}); diff --git a/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/email.ts b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/email.ts new file mode 100644 index 0000000000000..f22fe0e3bc1e7 --- /dev/null +++ b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/email.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function emailTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + describe('create email action', () => { + it('should return 403 when creating an email action', async () => { + await supertest + .post('/api/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'An email action', + actionTypeId: '.email', + config: { + service: '__json', + from: 'bob@example.com', + }, + secrets: { + user: 'bob', + password: 'supersecret', + }, + }) + .expect(403, { + statusCode: 403, + error: 'Forbidden', + message: + 'Action type .email is disabled because your basic license does not support it. Please upgrade your license.', + }); + }); + }); +} diff --git a/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/es_index.ts b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/es_index.ts new file mode 100644 index 0000000000000..ec07f6ff44df6 --- /dev/null +++ b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/es_index.ts @@ -0,0 +1,30 @@ +/* + * 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 '../../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function indexTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + describe('index action', () => { + it('should return 200 when creating an index action', async () => { + // create action with no config + await supertest + .post('/api/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'An index action', + actionTypeId: '.index', + config: { + index: 'foo', + }, + secrets: {}, + }) + .expect(200); + }); + }); +} diff --git a/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/pagerduty.ts b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/pagerduty.ts new file mode 100644 index 0000000000000..e261cf15d05ae --- /dev/null +++ b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/pagerduty.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function pagerdutyTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + describe('pagerduty action', () => { + it('should return 403 when creating a pagerduty action', async () => { + await supertest + .post('/api/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A pagerduty action', + actionTypeId: '.pagerduty', + config: { + apiUrl: 'http://localhost', + }, + secrets: { + routingKey: 'pager-duty-routing-key', + }, + }) + .expect(403, { + statusCode: 403, + error: 'Forbidden', + message: + 'Action type .pagerduty is disabled because your basic license does not support it. Please upgrade your license.', + }); + }); + }); +} diff --git a/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/server_log.ts b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/server_log.ts new file mode 100644 index 0000000000000..686f4a0086fa0 --- /dev/null +++ b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/server_log.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function serverLogTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + describe('server-log action', () => { + after(() => esArchiver.unload('empty_kibana')); + + it('should return 200 when creating a server-log action', async () => { + await supertest + .post('/api/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A server.log action', + actionTypeId: '.server-log', + }) + .expect(200); + }); + }); +} diff --git a/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/servicenow.ts b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/servicenow.ts new file mode 100644 index 0000000000000..a7551ad7e2fad --- /dev/null +++ b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/servicenow.ts @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { + getExternalServiceSimulatorPath, + ExternalServiceSimulator, +} from '../../../../common/fixtures/plugins/actions'; + +// node ../scripts/functional_test_runner.js --grep "Actions.servicenddd" --config=test/alerting_api_integration/security_and_spaces/config.ts + +const mapping = [ + { + source: 'title', + target: 'description', + actionType: 'nothing', + }, + { + source: 'description', + target: 'short_description', + actionType: 'nothing', + }, + { + source: 'comments', + target: 'comments', + actionType: 'nothing', + }, +]; + +// eslint-disable-next-line import/no-default-export +export default function servicenowTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const kibanaServer = getService('kibanaServer'); + const mockServiceNow = { + config: { + apiUrl: 'www.servicenowisinkibanaactions.com', + casesConfiguration: { mapping: [...mapping] }, + }, + secrets: { + password: 'elastic', + username: 'changeme', + }, + params: { + comments: 'hello cool service now incident', + short_description: 'this is a cool service now incident', + }, + }; + describe('servicenow', () => { + let servicenowSimulatorURL: string = ''; + + // need to wait for kibanaServer to settle ... + before(() => { + servicenowSimulatorURL = kibanaServer.resolveUrl( + getExternalServiceSimulatorPath(ExternalServiceSimulator.SERVICENOW) + ); + }); + + it('should return 403 when creating a servicenow action', async () => { + await supertest + .post('/api/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A servicenow action', + actionTypeId: '.servicenow', + config: { + apiUrl: servicenowSimulatorURL, + casesConfiguration: { ...mockServiceNow.config.casesConfiguration }, + }, + secrets: mockServiceNow.secrets, + }) + .expect(403, { + statusCode: 403, + error: 'Forbidden', + message: + 'Action type .servicenow is disabled because your basic license does not support it. Please upgrade your license.', + }); + }); + }); +} diff --git a/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/slack.ts b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/slack.ts new file mode 100644 index 0000000000000..46258e41d5d69 --- /dev/null +++ b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/slack.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { + getExternalServiceSimulatorPath, + ExternalServiceSimulator, +} from '../../../../common/fixtures/plugins/actions'; + +// eslint-disable-next-line import/no-default-export +export default function slackTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const kibanaServer = getService('kibanaServer'); + + describe('slack action', () => { + let slackSimulatorURL: string = ''; + + // need to wait for kibanaServer to settle ... + before(() => { + slackSimulatorURL = kibanaServer.resolveUrl( + getExternalServiceSimulatorPath(ExternalServiceSimulator.SLACK) + ); + }); + + it('should return 403 when creating a slack action', async () => { + await supertest + .post('/api/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A slack action', + actionTypeId: '.slack', + secrets: { + webhookUrl: slackSimulatorURL, + }, + }) + .expect(403, { + statusCode: 403, + error: 'Forbidden', + message: + 'Action type .slack is disabled because your basic license does not support it. Please upgrade your license.', + }); + }); + }); +} diff --git a/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/webhook.ts b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/webhook.ts new file mode 100644 index 0000000000000..338610e9243a4 --- /dev/null +++ b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/webhook.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { + getExternalServiceSimulatorPath, + ExternalServiceSimulator, +} from '../../../../common/fixtures/plugins/actions'; + +// eslint-disable-next-line import/no-default-export +export default function webhookTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const kibanaServer = getService('kibanaServer'); + + describe('webhook action', () => { + let webhookSimulatorURL: string = ''; + + // need to wait for kibanaServer to settle ... + before(() => { + webhookSimulatorURL = kibanaServer.resolveUrl( + getExternalServiceSimulatorPath(ExternalServiceSimulator.WEBHOOK) + ); + }); + + it('should return 403 when creating a webhook action', async () => { + await supertest + .post('/api/action') + .set('kbn-xsrf', 'test') + .send({ + name: 'A generic Webhook action', + actionTypeId: '.webhook', + secrets: { + user: 'username', + password: 'mypassphrase', + }, + config: { + url: webhookSimulatorURL, + }, + }) + .expect(403, { + statusCode: 403, + error: 'Forbidden', + message: + 'Action type .webhook is disabled because your basic license does not support it. Please upgrade your license.', + }); + }); + }); +} diff --git a/x-pack/test/alerting_api_integration/basic/tests/actions/index.ts b/x-pack/test/alerting_api_integration/basic/tests/actions/index.ts new file mode 100644 index 0000000000000..1788a12afebf2 --- /dev/null +++ b/x-pack/test/alerting_api_integration/basic/tests/actions/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function actionsTests({ loadTestFile }: FtrProviderContext) { + describe('Actions', () => { + loadTestFile(require.resolve('./builtin_action_types/email')); + loadTestFile(require.resolve('./builtin_action_types/es_index')); + loadTestFile(require.resolve('./builtin_action_types/pagerduty')); + loadTestFile(require.resolve('./builtin_action_types/server_log')); + loadTestFile(require.resolve('./builtin_action_types/servicenow')); + loadTestFile(require.resolve('./builtin_action_types/slack')); + loadTestFile(require.resolve('./builtin_action_types/webhook')); + }); +} diff --git a/x-pack/test/alerting_api_integration/basic/tests/index.ts b/x-pack/test/alerting_api_integration/basic/tests/index.ts new file mode 100644 index 0000000000000..2aa5ddee11047 --- /dev/null +++ b/x-pack/test/alerting_api_integration/basic/tests/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function alertingApiIntegrationTests({ + loadTestFile, + getService, +}: FtrProviderContext) { + describe('alerting api integration basic license', function() { + this.tags('ciGroup3'); + + loadTestFile(require.resolve('./actions')); + }); +} diff --git a/x-pack/test/alerting_api_integration/common/config.ts b/x-pack/test/alerting_api_integration/common/config.ts index eb03aafc03d08..5fb1afa7d584f 100644 --- a/x-pack/test/alerting_api_integration/common/config.ts +++ b/x-pack/test/alerting_api_integration/common/config.ts @@ -62,7 +62,8 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) ssl, serverArgs: [ `xpack.license.self_generated.type=${license}`, - `xpack.security.enabled=${!disabledPlugins.includes('security') && license === 'trial'}`, + `xpack.security.enabled=${!disabledPlugins.includes('security') && + ['trial', 'basic'].includes(license)}`, ], }, kbnTestServer: { diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions/index.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions/index.ts index aeec07aba906c..acd14e8a2bf7b 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions/index.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions/index.ts @@ -43,6 +43,7 @@ export default function(kibana: any) { const notEnabledActionType: ActionType = { id: 'test.not-enabled', name: 'Test: Not Enabled', + minimumLicenseRequired: 'gold', async executor() { return { status: 'ok', actionId: '' }; }, diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/index.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/index.ts index 58f7a49720007..9b4a2d14de9ea 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/index.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/index.ts @@ -42,6 +42,7 @@ export default function(kibana: any) { const noopActionType: ActionType = { id: 'test.noop', name: 'Test: Noop', + minimumLicenseRequired: 'gold', async executor() { return { status: 'ok', actionId: '' }; }, @@ -49,6 +50,7 @@ export default function(kibana: any) { const indexRecordActionType: ActionType = { id: 'test.index-record', name: 'Test: Index Record', + minimumLicenseRequired: 'gold', validate: { params: schema.object({ index: schema.string(), @@ -80,6 +82,7 @@ export default function(kibana: any) { const failingActionType: ActionType = { id: 'test.failing', name: 'Test: Failing', + minimumLicenseRequired: 'gold', validate: { params: schema.object({ index: schema.string(), @@ -104,6 +107,7 @@ export default function(kibana: any) { const rateLimitedActionType: ActionType = { id: 'test.rate-limit', name: 'Test: Rate Limit', + minimumLicenseRequired: 'gold', maxAttempts: 2, validate: { params: schema.object({ @@ -133,6 +137,7 @@ export default function(kibana: any) { const authorizationActionType: ActionType = { id: 'test.authorization', name: 'Test: Authorization', + minimumLicenseRequired: 'gold', validate: { params: schema.object({ callClusterAuthorizationIndex: schema.string(), diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/create.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/create.ts index 1ce9a6ba3a040..43a3861491467 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/create.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/create.ts @@ -205,10 +205,10 @@ export default function createActionTests({ getService }: FtrProviderContext) { break; case 'superuser at space1': case 'space_1_all at space1': - expect(response.statusCode).to.eql(400); + expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ - statusCode: 400, - error: 'Bad Request', + statusCode: 403, + error: 'Forbidden', message: 'action type "test.not-enabled" is not enabled in the Kibana config xpack.actions.enabledActionTypes', }); diff --git a/x-pack/test/alerting_api_integration/spaces_only/config.ts b/x-pack/test/alerting_api_integration/spaces_only/config.ts index 44603cc95e5e0..c79c26ef68752 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/config.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/config.ts @@ -7,4 +7,4 @@ import { createTestConfig } from '../common/config'; // eslint-disable-next-line import/no-default-export -export default createTestConfig('spaces_only', { disabledPlugins: ['security'], license: 'basic' }); +export default createTestConfig('spaces_only', { disabledPlugins: ['security'], license: 'trial' }); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/type_not_enabled.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/type_not_enabled.ts index 7193a80b94498..1388108806c0f 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/type_not_enabled.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/type_not_enabled.ts @@ -29,10 +29,10 @@ export default function typeNotEnabledTests({ getService }: FtrProviderContext) actionTypeId: DISABLED_ACTION_TYPE, }); - expect(response.statusCode).to.eql(400); + expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ - statusCode: 400, - error: 'Bad Request', + statusCode: 403, + error: 'Forbidden', message: 'action type "test.not-enabled" is not enabled in the Kibana config xpack.actions.enabledActionTypes', }); @@ -46,11 +46,10 @@ export default function typeNotEnabledTests({ getService }: FtrProviderContext) params: {}, }); - expect(response.statusCode).to.eql(200); + expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ - status: 'error', - retry: false, - actionId: PREWRITTEN_ACTION_ID, + statusCode: 403, + error: 'Forbidden', message: 'action type "test.not-enabled" is not enabled in the Kibana config xpack.actions.enabledActionTypes', }); @@ -76,12 +75,12 @@ export default function typeNotEnabledTests({ getService }: FtrProviderContext) name: 'an action created before test.not-enabled was disabled (updated)', }); - expect(responseUpdate.statusCode).to.eql(200); + expect(responseUpdate.statusCode).to.eql(403); expect(responseUpdate.body).to.eql({ - actionTypeId: 'test.not-enabled', - config: {}, - id: 'uuid-actionId', - name: 'an action created before test.not-enabled was disabled (updated)', + statusCode: 403, + error: 'Forbidden', + message: + 'action type "test.not-enabled" is not enabled in the Kibana config xpack.actions.enabledActionTypes', }); const response = await supertest.get(`/api/action/${PREWRITTEN_ACTION_ID}`); @@ -90,7 +89,7 @@ export default function typeNotEnabledTests({ getService }: FtrProviderContext) actionTypeId: 'test.not-enabled', config: {}, id: 'uuid-actionId', - name: 'an action created before test.not-enabled was disabled (updated)', + name: 'an action created before test.not-enabled was disabled', }); }); From 4c19cad11ba60dcbdf6e47fc66ed338c678e3f2b Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Fri, 20 Mar 2020 14:50:35 +0000 Subject: [PATCH 6/9] [Alerting] prevent flickering when fields are updated in an alert (#60666) This addresses the flickering in the graph when updating the Alert Add & Edit forms and adds an automatic refresh of the graph every 5 seconds. --- .../threshold/visualization.tsx | 56 +++++++++++++++---- 1 file changed, 45 insertions(+), 11 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/visualization.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/visualization.tsx index 0bcaa83127468..a87ff8bf4c312 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/visualization.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/visualization.tsx @@ -7,6 +7,7 @@ import React, { Fragment, useEffect, useState } from 'react'; import { IUiSettingsClient, HttpSetup } from 'kibana/public'; import { i18n } from '@kbn/i18n'; +import { interval } from 'rxjs'; import { AnnotationDomainTypes, Axis, @@ -21,7 +22,14 @@ import { niceTimeFormatter, } from '@elastic/charts'; import moment from 'moment-timezone'; -import { EuiCallOut, EuiLoadingChart, EuiSpacer, EuiEmptyPrompt, EuiText } from '@elastic/eui'; +import { + EuiCallOut, + EuiLoadingChart, + EuiSpacer, + EuiEmptyPrompt, + EuiText, + EuiLoadingSpinner, +} from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { getThresholdAlertVisualizationData } from '../../../../common/lib/index_threshold_api'; import { AggregationType, Comparator } from '../../../../common/types'; @@ -59,7 +67,7 @@ const getTimezone = (uiSettings: IUiSettingsClient) => { return tzOffset; }; -const getDomain = (alertInterval: string) => { +const getDomain = (alertInterval: string, startAt: Date) => { const VISUALIZE_INTERVALS = 30; let intervalMillis: number; @@ -69,10 +77,9 @@ const getDomain = (alertInterval: string) => { intervalMillis = 1000 * 60; // default to one minute if not parseable } - const now = Date.now(); return { - min: now - intervalMillis * VISUALIZE_INTERVALS, - max: now, + min: startAt.getTime() - intervalMillis * VISUALIZE_INTERVALS, + max: startAt.getTime(), }; }; @@ -84,6 +91,15 @@ interface Props { [key: string]: Comparator; }; alertsContext: AlertsContextValue; + refreshRateInMilliseconds?: number; +} + +const DEFAULT_REFRESH_RATE = 5000; + +enum LoadingStateType { + FirstLoad, + Refresh, + Idle, } type MetricResult = [number, number]; // [epochMillis, value] @@ -93,6 +109,7 @@ export const ThresholdVisualization: React.FunctionComponent = ({ aggregationTypes, comparators, alertsContext, + refreshRateInMilliseconds = DEFAULT_REFRESH_RATE, }) => { const { index, @@ -109,14 +126,25 @@ export const ThresholdVisualization: React.FunctionComponent = ({ } = alertParams; const { http, toastNotifications, charts, uiSettings, dataFieldsFormats } = alertsContext; - const [isLoading, setIsLoading] = useState(false); + const [loadingState, setLoadingState] = useState(null); const [error, setError] = useState(undefined); const [visualizationData, setVisualizationData] = useState>(); + const [startVisualizationAt, setStartVisualizationAt] = useState(new Date()); + + useEffect(() => { + const source = interval(refreshRateInMilliseconds); + const subscription = source.subscribe((val: number) => { + setStartVisualizationAt(new Date()); + }); + return () => { + subscription.unsubscribe(); + }; + }, [refreshRateInMilliseconds]); useEffect(() => { (async () => { try { - setIsLoading(true); + setLoadingState(loadingState ? LoadingStateType.Refresh : LoadingStateType.FirstLoad); setVisualizationData( await getVisualizationData(alertWithoutActions, visualizeOptions, http) ); @@ -131,7 +159,7 @@ export const ThresholdVisualization: React.FunctionComponent = ({ } setError(e); } finally { - setIsLoading(false); + setLoadingState(LoadingStateType.Idle); } })(); /* eslint-disable react-hooks/exhaustive-deps */ @@ -147,6 +175,7 @@ export const ThresholdVisualization: React.FunctionComponent = ({ timeWindowUnit, groupBy, threshold, + startVisualizationAt, ]); /* eslint-enable react-hooks/exhaustive-deps */ @@ -155,7 +184,7 @@ export const ThresholdVisualization: React.FunctionComponent = ({ } const chartsTheme = charts.theme.useChartsTheme(); - const domain = getDomain(alertInterval); + const domain = getDomain(alertInterval, startVisualizationAt); const visualizeOptions = { rangeFrom: new Date(domain.min).toISOString(), rangeTo: new Date(domain.max).toISOString(), @@ -165,7 +194,7 @@ export const ThresholdVisualization: React.FunctionComponent = ({ // Fetching visualization data is independent of alert actions const alertWithoutActions = { ...alertParams, actions: [], type: 'threshold' }; - if (isLoading) { + if (loadingState === LoadingStateType.FirstLoad) { return ( } @@ -224,7 +253,12 @@ export const ThresholdVisualization: React.FunctionComponent = ({ const dateFormatter = niceTimeFormatter([domain.min, domain.max]); const aggLabel = aggregationTypes[aggType].text; return ( -
+
+ {loadingState === LoadingStateType.Refresh ? ( + + ) : ( + + )} {alertVisualizationDataKeys.length ? ( Date: Fri, 20 Mar 2020 15:57:07 +0100 Subject: [PATCH 7/9] [SIEM] Fix types in rules tests (#60736) * [SIEM] Fix types in rules tests * Update create_rules.test.ts * Update create_rules.test.ts --- .../siem/server/lib/detection_engine/rules/create_rules.test.ts | 2 ++ .../siem/server/lib/detection_engine/rules/update_rules.test.ts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules.test.ts index 4c8d0f51f251b..14b8ffdfdacec 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules.test.ts @@ -28,10 +28,12 @@ describe('createRules', () => { await createRules({ alertsClient, actionsClient, + actions: [], ...params, ruleId: 'new-rule-id', enabled: true, interval: '', + throttle: null, name: '', tags: [], }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.test.ts index 5ee740a8b8845..967a32df20c3b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.test.ts @@ -33,10 +33,12 @@ describe('updateRules', () => { await updateRules({ alertsClient, actionsClient, + actions: [], savedObjectsClient, id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', ...params, enabled: true, + throttle: null, interval: '', name: '', tags: [], From 1a1e2e7b2e3d535339a8b7c486e189e7936f02cf Mon Sep 17 00:00:00 2001 From: Daniil Suleiman <31325372+sulemanof@users.noreply.github.com> Date: Fri, 20 Mar 2020 18:39:44 +0300 Subject: [PATCH 8/9] [NP] Remove `ui/agg_types` dependencies and move paginated table to kibana_legacy (#60276) * fix agg type shims and move paginated table to kibana_legacy * fix types * fix i18n ids * fix unit tests * Update imports * Remove ui/agg_types imports * Clean up vis_default_editor plugin * Remove agg_types imports in vis_type_table * Clean up x-pack * Clean up vis_type_vislib * Last cleanups * Update docs * Mock Schemas in vis_type_metric * Use data plugin mocks * Remove ui/directives/paginate reference * Remove snapshot * Remove shallow Co-authored-by: Joe Reuter Co-authored-by: Elastic Machine --- ...in-plugins-data-public.querystringinput.md | 2 +- ...na-plugin-plugins-data-public.searchbar.md | 4 +- src/dev/jest/config.js | 2 - .../np_ready/dashboard_state.test.ts | 7 - .../kibana/public/discover/kibana_services.ts | 1 - .../discover/np_ready/angular/discover.js | 4 +- .../core_plugins/kibana/public/kibana.js | 1 - .../region_map/public/region_map_type.js | 2 +- .../tile_map/public/tile_map_type.js | 2 +- .../public/directives/saved_object_finder.js | 534 +++++++++--------- .../__snapshots__/agg_params.test.tsx.snap | 70 --- .../public/components/agg.test.tsx | 5 +- .../public/components/agg.tsx | 2 +- .../public/components/agg_add.tsx | 2 +- .../public/components/agg_common_props.ts | 2 +- .../public/components/agg_group.test.tsx | 13 +- .../public/components/agg_group.tsx | 4 +- .../components/agg_group_helper.test.ts | 2 +- .../public/components/agg_group_helper.tsx | 2 +- .../public/components/agg_group_state.tsx | 2 +- .../public/components/agg_param_props.ts | 3 +- .../public/components/agg_params.test.tsx | 24 +- .../public/components/agg_params.tsx | 39 +- .../components/agg_params_helper.test.ts | 49 +- .../public/components/agg_params_helper.ts | 37 +- .../public/components/agg_params_map.ts | 7 +- .../public/components/agg_select.tsx | 3 +- .../components/controls/agg_control_props.tsx | 2 +- .../components/controls/agg_utils.test.tsx | 2 +- .../controls/components/mask_list.tsx | 4 +- .../public/components/controls/field.test.tsx | 3 +- .../public/components/controls/field.tsx | 3 +- .../public/components/controls/filter.tsx | 3 +- .../controls/has_extended_bounds.tsx | 4 +- .../components/controls/metric_agg.test.tsx | 2 +- .../components/controls/missing_bucket.tsx | 4 +- .../public/components/controls/order.tsx | 2 +- .../components/controls/order_agg.test.tsx | 2 - .../public/components/controls/order_agg.tsx | 2 +- .../public/components/controls/order_by.tsx | 3 +- .../components/controls/percentiles.test.tsx | 2 +- .../public/components/controls/sub_agg.tsx | 2 +- .../public/components/controls/sub_metric.tsx | 2 +- .../public/components/controls/test_utils.ts | 2 +- .../components/controls/time_interval.tsx | 6 +- .../controls/top_aggregate.test.tsx | 2 +- .../components/controls/top_aggregate.tsx | 2 +- .../components/controls/utils/agg_utils.ts | 2 +- .../components/controls/utils/use_handlers.ts | 2 +- .../public/components/sidebar/data_tab.tsx | 8 +- .../public/components/sidebar/sidebar.tsx | 2 +- .../components/sidebar/state/actions.ts | 2 +- .../public/components/sidebar/state/index.ts | 11 +- .../components/sidebar/state/reducers.ts | 10 +- .../public/legacy_imports.ts | 47 -- .../vis_default_editor/public/types.ts | 24 + .../vis_default_editor/public/utils.test.ts | 4 +- .../public/vis_options_props.tsx | 2 +- .../public/vis_type_agg_filter.ts | 4 +- .../vis_type_metric/public/legacy_imports.ts | 1 - .../public/metric_vis_fn.test.ts | 17 +- .../public/metric_vis_type.test.ts | 17 +- .../vis_type_metric/public/metric_vis_type.ts | 3 +- .../public/agg_table/__tests__/agg_table.js | 5 +- .../agg_table/__tests__/agg_table_group.js | 5 +- .../public/components/table_vis_options.tsx | 4 +- .../public/get_inner_angular.ts | 6 +- .../vis_type_table/public/legacy_imports.ts | 13 - .../public/table_vis_controller.test.ts | 9 +- .../vis_type_table/public/table_vis_type.ts | 3 +- .../public/legacy_imports.ts | 1 - .../public/tag_cloud_type.ts | 2 +- .../vis_type_vislib/public/area.ts | 3 +- .../components/options/gauge/style_panel.tsx | 2 +- .../options/metrics_axes/index.test.tsx | 3 +- .../components/options/metrics_axes/index.tsx | 2 +- .../vis_type_vislib/public/gauge.ts | 4 +- .../vis_type_vislib/public/goal.ts | 3 +- .../vis_type_vislib/public/heatmap.ts | 4 +- .../vis_type_vislib/public/histogram.ts | 4 +- .../vis_type_vislib/public/horizontal_bar.ts | 4 +- .../vis_type_vislib/public/legacy_imports.ts | 1 - .../vis_type_vislib/public/line.ts | 3 +- .../vis_type_vislib/public/pie.ts | 3 +- src/legacy/ui/public/agg_types/index.ts | 85 --- .../common/field_formats/utils/serialize.ts | 2 +- src/plugins/data/public/public.api.md | 6 +- src/plugins/kibana_legacy/public/index.ts | 1 + .../public/paginate/paginate.d.ts | 21 + .../public/paginate}/paginate.js | 17 +- .../public/paginate}/paginate_controls.html | 4 +- .../dashboard_mode/public/dashboard_viewer.js | 1 - .../operations/definitions/date_histogram.tsx | 9 +- x-pack/legacy/plugins/rollup/public/legacy.ts | 6 +- .../plugins/rollup/public/legacy_imports.ts | 3 +- x-pack/legacy/plugins/rollup/public/plugin.ts | 18 +- .../translations/translations/ja-JP.json | 6 +- .../translations/translations/zh-CN.json | 6 +- 98 files changed, 575 insertions(+), 729 deletions(-) delete mode 100644 src/legacy/core_plugins/vis_default_editor/public/components/__snapshots__/agg_params.test.tsx.snap delete mode 100644 src/legacy/core_plugins/vis_default_editor/public/legacy_imports.ts create mode 100644 src/legacy/core_plugins/vis_default_editor/public/types.ts delete mode 100644 src/legacy/ui/public/agg_types/index.ts create mode 100644 src/plugins/kibana_legacy/public/paginate/paginate.d.ts rename src/{legacy/ui/public/directives => plugins/kibana_legacy/public/paginate}/paginate.js (93%) rename src/{legacy/ui/public/directives/partials => plugins/kibana_legacy/public/paginate}/paginate_controls.html (96%) diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinput.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinput.md index 58690300b3bd6..d0d4cc491e142 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinput.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinput.md @@ -7,5 +7,5 @@ Signature: ```typescript -QueryStringInput: React.FC> +QueryStringInput: React.FC> ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchbar.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchbar.md index 5cdf938a9e47f..89c5ca800a4d4 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchbar.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchbar.md @@ -7,7 +7,7 @@ Signature: ```typescript -SearchBar: React.ComponentClass, "query" | "isLoading" | "indexPatterns" | "filters" | "onQueryChange" | "customSubmitButton" | "screenTitle" | "dataTestSubj" | "showQueryBar" | "showQueryInput" | "showFilterBar" | "showDatePicker" | "showAutoRefreshOnly" | "isRefreshPaused" | "refreshInterval" | "dateRangeFrom" | "dateRangeTo" | "showSaveQuery" | "savedQuery" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated" | "onClearSavedQuery" | "onRefresh" | "timeHistory" | "onFiltersUpdated" | "onRefreshChange">, any> & { - WrappedComponent: React.ComponentType & ReactIntl.InjectedIntlProps>; +SearchBar: React.ComponentClass, "query" | "isLoading" | "indexPatterns" | "filters" | "refreshInterval" | "screenTitle" | "dataTestSubj" | "customSubmitButton" | "showQueryBar" | "showQueryInput" | "showFilterBar" | "showDatePicker" | "showAutoRefreshOnly" | "isRefreshPaused" | "dateRangeFrom" | "dateRangeTo" | "showSaveQuery" | "savedQuery" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated" | "onClearSavedQuery" | "onRefresh" | "timeHistory" | "onFiltersUpdated" | "onRefreshChange">, any> & { + WrappedComponent: React.ComponentType & ReactIntl.InjectedIntlProps>; } ``` diff --git a/src/dev/jest/config.js b/src/dev/jest/config.js index 8078c32b10646..a941735c7840e 100644 --- a/src/dev/jest/config.js +++ b/src/dev/jest/config.js @@ -49,8 +49,6 @@ export default { '!packages/kbn-ui-framework/src/services/**/*/index.js', 'src/legacy/core_plugins/**/*.{js,jsx,ts,tsx}', '!src/legacy/core_plugins/**/{__test__,__snapshots__}/**/*', - 'src/legacy/ui/public/{agg_types,vis}/**/*.{ts,tsx}', - '!src/legacy/ui/public/{agg_types,vis}/**/*.d.ts', ], moduleNameMapper: { '@elastic/eui$': '/node_modules/@elastic/eui/test-env', diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_state.test.ts b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_state.test.ts index 60ea14dad19e1..08ccc1e0d1e89 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_state.test.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_state.test.ts @@ -25,13 +25,6 @@ import { InputTimeRange, TimefilterContract, TimeRange } from 'src/plugins/data/ import { ViewMode } from 'src/plugins/embeddable/public'; import { createKbnUrlStateStorage } from 'src/plugins/kibana_utils/public'; -jest.mock('ui/agg_types', () => ({ - aggTypes: { - metrics: [], - buckets: [], - }, -})); - describe('DashboardState', function() { let dashboardState: DashboardStateManager; const savedDashboard = getSavedDashboardMock(); 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 725e94f16e2e8..cf76a9355e384 100644 --- a/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts +++ b/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts @@ -53,7 +53,6 @@ export { wrapInI18nContext } from 'ui/i18n'; import { search } from '../../../../../plugins/data/public'; export const { getRequestInspectorStats, getResponseInspectorStats, tabifyAggResponse } = search; // @ts-ignore -export { intervalOptions } from 'ui/agg_types'; // @ts-ignore export { timezoneProvider } from 'ui/vis/lib/timezone'; export { diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js index 2857f8720d8dc..e45ab2a7d7675 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js @@ -44,7 +44,6 @@ import { getRequestInspectorStats, getResponseInspectorStats, getServices, - intervalOptions, unhashUrl, subscribeWithScope, tabifyAggResponse, @@ -76,6 +75,7 @@ import { connectToQueryState, syncQueryStateWithUrl, getDefaultQuery, + search, } from '../../../../../../../plugins/data/public'; import { getIndexPatternId } from '../helpers/get_index_pattern_id'; import { addFatalError } from '../../../../../../../plugins/kibana_legacy/public'; @@ -285,7 +285,7 @@ function discoverController( mode: 'absolute', }); }; - $scope.intervalOptions = intervalOptions; + $scope.intervalOptions = search.aggs.intervalOptions; $scope.minimumVisibleRows = 50; $scope.fetchStatus = fetchStatuses.UNINITIALIZED; $scope.showSaveQuery = uiCapabilities.discover.saveQuery; diff --git a/src/legacy/core_plugins/kibana/public/kibana.js b/src/legacy/core_plugins/kibana/public/kibana.js index 04eaf2cbe2679..df6b08ef76556 100644 --- a/src/legacy/core_plugins/kibana/public/kibana.js +++ b/src/legacy/core_plugins/kibana/public/kibana.js @@ -48,7 +48,6 @@ import './dashboard/legacy'; import './management'; import './dev_tools'; import 'ui/agg_response'; -import 'ui/agg_types'; import { showAppRedirectNotification } from '../../../../plugins/kibana_legacy/public'; import 'leaflet'; import { localApplicationService } from './local_application_service'; diff --git a/src/legacy/core_plugins/region_map/public/region_map_type.js b/src/legacy/core_plugins/region_map/public/region_map_type.js index a03fbe4b291e2..9a1a76362e094 100644 --- a/src/legacy/core_plugins/region_map/public/region_map_type.js +++ b/src/legacy/core_plugins/region_map/public/region_map_type.js @@ -18,12 +18,12 @@ */ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { Schemas } from 'ui/agg_types'; import { mapToLayerWithId } from './util'; import { createRegionMapVisualization } from './region_map_visualization'; import { Status } from '../../visualizations/public'; import { RegionMapOptions } from './components/region_map_options'; import { truncatedColorSchemas } from '../../../../plugins/charts/public'; +import { Schemas } from '../../vis_default_editor/public'; // TODO: reference to TILE_MAP plugin should be removed import { ORIGIN } from '../../tile_map/common/origin'; diff --git a/src/legacy/core_plugins/tile_map/public/tile_map_type.js b/src/legacy/core_plugins/tile_map/public/tile_map_type.js index 544b63abe82c7..0809bf6ecbab6 100644 --- a/src/legacy/core_plugins/tile_map/public/tile_map_type.js +++ b/src/legacy/core_plugins/tile_map/public/tile_map_type.js @@ -21,8 +21,8 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { convertToGeoJson } from 'ui/vis/map/convert_to_geojson'; -import { Schemas } from 'ui/agg_types'; +import { Schemas } from '../../vis_default_editor/public'; import { Status } from '../../visualizations/public'; import { createTileMapVisualization } from './tile_map_visualization'; import { TileMapOptions } from './components/tile_map_options'; diff --git a/src/legacy/core_plugins/timelion/public/directives/saved_object_finder.js b/src/legacy/core_plugins/timelion/public/directives/saved_object_finder.js index 111db0a83ffc4..fb54c36df27d7 100644 --- a/src/legacy/core_plugins/timelion/public/directives/saved_object_finder.js +++ b/src/legacy/core_plugins/timelion/public/directives/saved_object_finder.js @@ -21,287 +21,295 @@ import _ from 'lodash'; import rison from 'rison-node'; import { uiModules } from 'ui/modules'; import 'ui/directives/input_focus'; -import 'ui/directives/paginate'; import savedObjectFinderTemplate from './saved_object_finder.html'; import { savedSheetLoader } from '../services/saved_sheets'; import { keyMap } from 'ui/directives/key_map'; +import { + PaginateControlsDirectiveProvider, + PaginateDirectiveProvider, +} from '../../../../../plugins/kibana_legacy/public'; const module = uiModules.get('kibana'); -module.directive('savedObjectFinder', function($location, kbnUrl, Private, config) { - return { - restrict: 'E', - scope: { - type: '@', - // optional make-url attr, sets the userMakeUrl in our scope - userMakeUrl: '=?makeUrl', - // optional on-choose attr, sets the userOnChoose in our scope - userOnChoose: '=?onChoose', - // optional useLocalManagement attr, removes link to management section - useLocalManagement: '=?useLocalManagement', - /** - * @type {function} - an optional function. If supplied an `Add new X` button is shown - * and this function is called when clicked. - */ - onAddNew: '=', - /** - * @{type} boolean - set this to true, if you don't want the search box above the - * table to automatically gain focus once loaded - */ - disableAutoFocus: '=', - }, - template: savedObjectFinderTemplate, - controllerAs: 'finder', - controller: function($scope, $element) { - const self = this; - - // the text input element - const $input = $element.find('input[ng-model=filter]'); - - // The number of items to show in the list - $scope.perPage = config.get('savedObjects:perPage'); - - // the list that will hold the suggestions - const $list = $element.find('ul'); - - // the current filter string, used to check that returned results are still useful - let currentFilter = $scope.filter; - - // the most recently entered search/filter - let prevSearch; - - // the list of hits, used to render display - self.hits = []; - - self.service = savedSheetLoader; - self.properties = self.service.loaderProperties; - - filterResults(); - - /** - * Boolean that keeps track of whether hits are sorted ascending (true) - * or descending (false) by title - * @type {Boolean} - */ - self.isAscending = true; - - /** - * Sorts saved object finder hits either ascending or descending - * @param {Array} hits Array of saved finder object hits - * @return {Array} Array sorted either ascending or descending - */ - self.sortHits = function(hits) { - self.isAscending = !self.isAscending; - self.hits = self.isAscending ? _.sortBy(hits, 'title') : _.sortBy(hits, 'title').reverse(); - }; - - /** - * Passed the hit objects and will determine if the - * hit should have a url in the UI, returns it if so - * @return {string|null} - the url or nothing - */ - self.makeUrl = function(hit) { - if ($scope.userMakeUrl) { - return $scope.userMakeUrl(hit); - } - - if (!$scope.userOnChoose) { - return hit.url; - } +module + .directive('paginate', PaginateDirectiveProvider) + .directive('paginateControls', PaginateControlsDirectiveProvider) + .directive('savedObjectFinder', function($location, kbnUrl, Private, config) { + return { + restrict: 'E', + scope: { + type: '@', + // optional make-url attr, sets the userMakeUrl in our scope + userMakeUrl: '=?makeUrl', + // optional on-choose attr, sets the userOnChoose in our scope + userOnChoose: '=?onChoose', + // optional useLocalManagement attr, removes link to management section + useLocalManagement: '=?useLocalManagement', + /** + * @type {function} - an optional function. If supplied an `Add new X` button is shown + * and this function is called when clicked. + */ + onAddNew: '=', + /** + * @{type} boolean - set this to true, if you don't want the search box above the + * table to automatically gain focus once loaded + */ + disableAutoFocus: '=', + }, + template: savedObjectFinderTemplate, + controllerAs: 'finder', + controller: function($scope, $element) { + const self = this; + + // the text input element + const $input = $element.find('input[ng-model=filter]'); + + // The number of items to show in the list + $scope.perPage = config.get('savedObjects:perPage'); + + // the list that will hold the suggestions + const $list = $element.find('ul'); + + // the current filter string, used to check that returned results are still useful + let currentFilter = $scope.filter; + + // the most recently entered search/filter + let prevSearch; + + // the list of hits, used to render display + self.hits = []; + + self.service = savedSheetLoader; + self.properties = self.service.loaderProperties; - return '#'; - }; + filterResults(); - self.preventClick = function($event) { - $event.preventDefault(); - }; + /** + * Boolean that keeps track of whether hits are sorted ascending (true) + * or descending (false) by title + * @type {Boolean} + */ + self.isAscending = true; + + /** + * Sorts saved object finder hits either ascending or descending + * @param {Array} hits Array of saved finder object hits + * @return {Array} Array sorted either ascending or descending + */ + self.sortHits = function(hits) { + self.isAscending = !self.isAscending; + self.hits = self.isAscending + ? _.sortBy(hits, 'title') + : _.sortBy(hits, 'title').reverse(); + }; + + /** + * Passed the hit objects and will determine if the + * hit should have a url in the UI, returns it if so + * @return {string|null} - the url or nothing + */ + self.makeUrl = function(hit) { + if ($scope.userMakeUrl) { + return $scope.userMakeUrl(hit); + } - /** - * Called when a hit object is clicked, can override the - * url behavior if necessary. - */ - self.onChoose = function(hit, $event) { - if ($scope.userOnChoose) { - $scope.userOnChoose(hit, $event); - } + if (!$scope.userOnChoose) { + return hit.url; + } - const url = self.makeUrl(hit); - if (!url || url === '#' || url.charAt(0) !== '#') return; + return '#'; + }; - $event.preventDefault(); + self.preventClick = function($event) { + $event.preventDefault(); + }; - // we want the '/path', not '#/path' - kbnUrl.change(url.substr(1)); - }; + /** + * Called when a hit object is clicked, can override the + * url behavior if necessary. + */ + self.onChoose = function(hit, $event) { + if ($scope.userOnChoose) { + $scope.userOnChoose(hit, $event); + } - $scope.$watch('filter', function(newFilter) { - // ensure that the currentFilter changes from undefined to '' - // which triggers - currentFilter = newFilter || ''; - filterResults(); - }); - - $scope.pageFirstItem = 0; - $scope.pageLastItem = 0; - $scope.onPageChanged = page => { - $scope.pageFirstItem = page.firstItem; - $scope.pageLastItem = page.lastItem; - }; - - //manages the state of the keyboard selector - self.selector = { - enabled: false, - index: -1, - }; - - self.getLabel = function() { - return _.words(self.properties.nouns) - .map(_.capitalize) - .join(' '); - }; - - //key handler for the filter text box - self.filterKeyDown = function($event) { - switch (keyMap[$event.keyCode]) { - case 'enter': - if (self.hitCount !== 1) return; - - const hit = self.hits[0]; - if (!hit) return; - - self.onChoose(hit, $event); - $event.preventDefault(); - break; - } - }; + const url = self.makeUrl(hit); + if (!url || url === '#' || url.charAt(0) !== '#') return; - //key handler for the list items - self.hitKeyDown = function($event, page, paginate) { - switch (keyMap[$event.keyCode]) { - case 'tab': - if (!self.selector.enabled) break; + $event.preventDefault(); - self.selector.index = -1; - self.selector.enabled = false; + // we want the '/path', not '#/path' + kbnUrl.change(url.substr(1)); + }; - //if the user types shift-tab return to the textbox - //if the user types tab, set the focus to the currently selected hit. - if ($event.shiftKey) { - $input.focus(); - } else { - $list.find('li.active a').focus(); - } + $scope.$watch('filter', function(newFilter) { + // ensure that the currentFilter changes from undefined to '' + // which triggers + currentFilter = newFilter || ''; + filterResults(); + }); - $event.preventDefault(); - break; - case 'down': - if (!self.selector.enabled) break; + $scope.pageFirstItem = 0; + $scope.pageLastItem = 0; + $scope.onPageChanged = page => { + $scope.pageFirstItem = page.firstItem; + $scope.pageLastItem = page.lastItem; + }; + + //manages the state of the keyboard selector + self.selector = { + enabled: false, + index: -1, + }; + + self.getLabel = function() { + return _.words(self.properties.nouns) + .map(_.capitalize) + .join(' '); + }; + + //key handler for the filter text box + self.filterKeyDown = function($event) { + switch (keyMap[$event.keyCode]) { + case 'enter': + if (self.hitCount !== 1) return; + + const hit = self.hits[0]; + if (!hit) return; + + self.onChoose(hit, $event); + $event.preventDefault(); + break; + } + }; + + //key handler for the list items + self.hitKeyDown = function($event, page, paginate) { + switch (keyMap[$event.keyCode]) { + case 'tab': + if (!self.selector.enabled) break; + + self.selector.index = -1; + self.selector.enabled = false; + + //if the user types shift-tab return to the textbox + //if the user types tab, set the focus to the currently selected hit. + if ($event.shiftKey) { + $input.focus(); + } else { + $list.find('li.active a').focus(); + } + + $event.preventDefault(); + break; + case 'down': + if (!self.selector.enabled) break; + + if (self.selector.index + 1 < page.length) { + self.selector.index += 1; + } + $event.preventDefault(); + break; + case 'up': + if (!self.selector.enabled) break; + + if (self.selector.index > 0) { + self.selector.index -= 1; + } + $event.preventDefault(); + break; + case 'right': + if (!self.selector.enabled) break; + + if (page.number < page.count) { + paginate.goToPage(page.number + 1); + self.selector.index = 0; + selectTopHit(); + } + $event.preventDefault(); + break; + case 'left': + if (!self.selector.enabled) break; + + if (page.number > 1) { + paginate.goToPage(page.number - 1); + self.selector.index = 0; + selectTopHit(); + } + $event.preventDefault(); + break; + case 'escape': + if (!self.selector.enabled) break; - if (self.selector.index + 1 < page.length) { - self.selector.index += 1; - } - $event.preventDefault(); - break; - case 'up': - if (!self.selector.enabled) break; + $input.focus(); + $event.preventDefault(); + break; + case 'enter': + if (!self.selector.enabled) break; + + const hitIndex = (page.number - 1) * paginate.perPage + self.selector.index; + const hit = self.hits[hitIndex]; + if (!hit) break; + + self.onChoose(hit, $event); + $event.preventDefault(); + break; + case 'shift': + break; + default: + $input.focus(); + break; + } + }; + + self.hitBlur = function() { + self.selector.index = -1; + self.selector.enabled = false; + }; + + self.manageObjects = function(type) { + $location.url('/management/kibana/objects?_a=' + rison.encode({ tab: type })); + }; + + self.hitCountNoun = function() { + return (self.hitCount === 1 ? self.properties.noun : self.properties.nouns).toLowerCase(); + }; + + function selectTopHit() { + setTimeout(function() { + //triggering a focus event kicks off a new angular digest cycle. + $list.find('a:first').focus(); + }, 0); + } - if (self.selector.index > 0) { - self.selector.index -= 1; - } - $event.preventDefault(); - break; - case 'right': - if (!self.selector.enabled) break; - - if (page.number < page.count) { - paginate.goToPage(page.number + 1); - self.selector.index = 0; - selectTopHit(); - } - $event.preventDefault(); - break; - case 'left': - if (!self.selector.enabled) break; - - if (page.number > 1) { - paginate.goToPage(page.number - 1); - self.selector.index = 0; - selectTopHit(); + function filterResults() { + if (!self.service) return; + if (!self.properties) return; + + // track the filter that we use for this search, + // but ensure that we don't search for the same + // thing twice. This is called from multiple places + // and needs to be smart about when it actually searches + const filter = currentFilter; + if (prevSearch === filter) return; + + prevSearch = filter; + + const isLabsEnabled = config.get('visualize:enableLabs'); + self.service.find(filter).then(function(hits) { + hits.hits = hits.hits.filter( + hit => isLabsEnabled || _.get(hit, 'type.stage') !== 'experimental' + ); + hits.total = hits.hits.length; + + // ensure that we don't display old results + // as we can't really cancel requests + if (currentFilter === filter) { + self.hitCount = hits.total; + self.hits = _.sortBy(hits.hits, 'title'); } - $event.preventDefault(); - break; - case 'escape': - if (!self.selector.enabled) break; - - $input.focus(); - $event.preventDefault(); - break; - case 'enter': - if (!self.selector.enabled) break; - - const hitIndex = (page.number - 1) * paginate.perPage + self.selector.index; - const hit = self.hits[hitIndex]; - if (!hit) break; - - self.onChoose(hit, $event); - $event.preventDefault(); - break; - case 'shift': - break; - default: - $input.focus(); - break; + }); } - }; - - self.hitBlur = function() { - self.selector.index = -1; - self.selector.enabled = false; - }; - - self.manageObjects = function(type) { - $location.url('/management/kibana/objects?_a=' + rison.encode({ tab: type })); - }; - - self.hitCountNoun = function() { - return (self.hitCount === 1 ? self.properties.noun : self.properties.nouns).toLowerCase(); - }; - - function selectTopHit() { - setTimeout(function() { - //triggering a focus event kicks off a new angular digest cycle. - $list.find('a:first').focus(); - }, 0); - } - - function filterResults() { - if (!self.service) return; - if (!self.properties) return; - - // track the filter that we use for this search, - // but ensure that we don't search for the same - // thing twice. This is called from multiple places - // and needs to be smart about when it actually searches - const filter = currentFilter; - if (prevSearch === filter) return; - - prevSearch = filter; - - const isLabsEnabled = config.get('visualize:enableLabs'); - self.service.find(filter).then(function(hits) { - hits.hits = hits.hits.filter( - hit => isLabsEnabled || _.get(hit, 'type.stage') !== 'experimental' - ); - hits.total = hits.hits.length; - - // ensure that we don't display old results - // as we can't really cancel requests - if (currentFilter === filter) { - self.hitCount = hits.total; - self.hits = _.sortBy(hits.hits, 'title'); - } - }); - } - }, - }; -}); + }, + }; + }); diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/__snapshots__/agg_params.test.tsx.snap b/src/legacy/core_plugins/vis_default_editor/public/components/__snapshots__/agg_params.test.tsx.snap deleted file mode 100644 index 028d0b8016693..0000000000000 --- a/src/legacy/core_plugins/vis_default_editor/public/components/__snapshots__/agg_params.test.tsx.snap +++ /dev/null @@ -1,70 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`DefaultEditorAggParams component should init with the default set of params 1`] = ` - - - - - - - - - -`; diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/agg.test.tsx b/src/legacy/core_plugins/vis_default_editor/public/components/agg.test.tsx index 22e0ebb3d30dc..7e715be25bff3 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/agg.test.tsx +++ b/src/legacy/core_plugins/vis_default_editor/public/components/agg.test.tsx @@ -21,17 +21,14 @@ import React from 'react'; import { mount, shallow } from 'enzyme'; import { act } from 'react-dom/test-utils'; -import { IndexPattern } from 'src/plugins/data/public'; +import { IndexPattern, IAggType, AggGroupNames } from 'src/plugins/data/public'; import { VisState } from 'src/legacy/core_plugins/visualizations/public'; -import { IAggType, AggGroupNames } from '../legacy_imports'; import { DefaultEditorAgg, DefaultEditorAggProps } from './agg'; import { DefaultEditorAggParams } from './agg_params'; import { AGGS_ACTION_KEYS } from './agg_group_state'; import { Schema } from '../schemas'; -jest.mock('ui/new_platform'); - jest.mock('./agg_params', () => ({ DefaultEditorAggParams: () => null, })); diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/agg.tsx b/src/legacy/core_plugins/vis_default_editor/public/components/agg.tsx index 30ccd4f0b6cae..2a45273207623 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/agg.tsx +++ b/src/legacy/core_plugins/vis_default_editor/public/components/agg.tsx @@ -28,7 +28,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { IAggConfig } from '../legacy_imports'; +import { IAggConfig } from 'src/plugins/data/public'; import { DefaultEditorAggParams } from './agg_params'; import { DefaultEditorAggCommonProps } from './agg_common_props'; import { AGGS_ACTION_KEYS, AggsAction } from './agg_group_state'; diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/agg_add.tsx b/src/legacy/core_plugins/vis_default_editor/public/components/agg_add.tsx index 24cb83498d4d0..9df4ea58e0f07 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/agg_add.tsx +++ b/src/legacy/core_plugins/vis_default_editor/public/components/agg_add.tsx @@ -29,7 +29,7 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { IAggConfig, AggGroupNames } from '../legacy_imports'; +import { IAggConfig, AggGroupNames } from '../../../../../plugins/data/public'; import { Schema } from '../schemas'; interface DefaultEditorAggAddProps { diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/agg_common_props.ts b/src/legacy/core_plugins/vis_default_editor/public/components/agg_common_props.ts index 1a97cc5c4d967..ec92f511b6eee 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/agg_common_props.ts +++ b/src/legacy/core_plugins/vis_default_editor/public/components/agg_common_props.ts @@ -18,7 +18,7 @@ */ import { VisState, VisParams } from 'src/legacy/core_plugins/visualizations/public'; -import { IAggType, IAggConfig, IAggGroupNames } from '../legacy_imports'; +import { IAggType, IAggConfig, IAggGroupNames } from 'src/plugins/data/public'; import { Schema } from '../schemas'; type AggId = IAggConfig['id']; diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/agg_group.test.tsx b/src/legacy/core_plugins/vis_default_editor/public/components/agg_group.test.tsx index ec467480539ab..63f5e696c99f4 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/agg_group.test.tsx +++ b/src/legacy/core_plugins/vis_default_editor/public/components/agg_group.test.tsx @@ -21,7 +21,7 @@ import React from 'react'; import { mount, shallow } from 'enzyme'; import { act } from 'react-dom/test-utils'; import { VisState } from 'src/legacy/core_plugins/visualizations/public'; -import { IAggConfigs, IAggConfig } from '../legacy_imports'; +import { IAggConfigs, IAggConfig } from 'src/plugins/data/public'; import { DefaultEditorAggGroup, DefaultEditorAggGroupProps } from './agg_group'; import { DefaultEditorAgg } from './agg'; import { DefaultEditorAggAdd } from './agg_add'; @@ -36,17 +36,6 @@ jest.mock('@elastic/eui', () => ({ EuiPanel: 'eui-panel', })); -jest.mock('../legacy_imports', () => ({ - aggGroupNamesMap: () => ({ - metrics: 'Metrics', - buckets: 'Buckets', - }), - AggGroupNames: { - Metrics: 'metrics', - Buckets: 'buckets', - }, -})); - jest.mock('./agg', () => ({ DefaultEditorAgg: () =>
, })); diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/agg_group.tsx b/src/legacy/core_plugins/vis_default_editor/public/components/agg_group.tsx index a15a98d4983ce..600612f2cf9d8 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/agg_group.tsx +++ b/src/legacy/core_plugins/vis_default_editor/public/components/agg_group.tsx @@ -30,7 +30,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { IAggConfig, aggGroupNamesMap, AggGroupNames } from '../legacy_imports'; +import { AggGroupNames, search, IAggConfig } from '../../../../../plugins/data/public'; import { DefaultEditorAgg } from './agg'; import { DefaultEditorAggAdd } from './agg_add'; import { AddSchema, ReorderAggs, DefaultEditorAggCommonProps } from './agg_common_props'; @@ -68,7 +68,7 @@ function DefaultEditorAggGroup({ setTouched, setValidity, }: DefaultEditorAggGroupProps) { - const groupNameLabel = (aggGroupNamesMap() as any)[groupName]; + const groupNameLabel = (search.aggs.aggGroupNamesMap() as any)[groupName]; // e.g. buckets can have no aggs const schemaNames = getSchemasByGroup(schemas, groupName).map(s => s.name); const group: IAggConfig[] = useMemo( diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/agg_group_helper.test.ts b/src/legacy/core_plugins/vis_default_editor/public/components/agg_group_helper.test.ts index aebece29e7ae6..3693f1b1e3091 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/agg_group_helper.test.ts +++ b/src/legacy/core_plugins/vis_default_editor/public/components/agg_group_helper.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { IAggConfig } from '../legacy_imports'; +import { IAggConfig } from 'src/plugins/data/public'; import { isAggRemovable, calcAggIsTooLow, diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/agg_group_helper.tsx b/src/legacy/core_plugins/vis_default_editor/public/components/agg_group_helper.tsx index 0a8c5c3077ada..9a4cca940baea 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/agg_group_helper.tsx +++ b/src/legacy/core_plugins/vis_default_editor/public/components/agg_group_helper.tsx @@ -18,7 +18,7 @@ */ import { findIndex, isEmpty } from 'lodash'; -import { IAggConfig } from '../legacy_imports'; +import { IAggConfig } from 'src/plugins/data/public'; import { AggsState } from './agg_group_state'; import { Schema, getSchemaByName } from '../schemas'; diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/agg_group_state.tsx b/src/legacy/core_plugins/vis_default_editor/public/components/agg_group_state.tsx index d022297ae72b3..bfd5bec339b1f 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/agg_group_state.tsx +++ b/src/legacy/core_plugins/vis_default_editor/public/components/agg_group_state.tsx @@ -17,7 +17,7 @@ * under the License. */ -import { IAggConfig } from '../legacy_imports'; +import { IAggConfig } from 'src/plugins/data/public'; export enum AGGS_ACTION_KEYS { TOUCHED = 'aggsTouched', diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/agg_param_props.ts b/src/legacy/core_plugins/vis_default_editor/public/components/agg_param_props.ts index cdc5a4c8f8a77..7c2852798b403 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/agg_param_props.ts +++ b/src/legacy/core_plugins/vis_default_editor/public/components/agg_param_props.ts @@ -17,9 +17,8 @@ * under the License. */ -import { IndexPatternField } from 'src/plugins/data/public'; +import { IAggConfig, AggParam, IndexPatternField } from 'src/plugins/data/public'; import { VisState } from 'src/legacy/core_plugins/visualizations/public'; -import { IAggConfig, AggParam } from '../legacy_imports'; import { ComboBoxGroupedOptions } from '../utils'; import { EditorConfig } from './utils'; import { Schema } from '../schemas'; diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/agg_params.test.tsx b/src/legacy/core_plugins/vis_default_editor/public/components/agg_params.test.tsx index d2821566fcb37..cd6486b6a1532 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/agg_params.test.tsx +++ b/src/legacy/core_plugins/vis_default_editor/public/components/agg_params.test.tsx @@ -18,12 +18,16 @@ */ import React from 'react'; -import { mount, shallow } from 'enzyme'; +import { mount } from 'enzyme'; import { VisState } from 'src/legacy/core_plugins/visualizations/public'; -import { IndexPattern } from 'src/plugins/data/public'; -import { DefaultEditorAggParams, DefaultEditorAggParamsProps } from './agg_params'; -import { IAggConfig, AggGroupNames } from '../legacy_imports'; +import { IndexPattern, IAggConfig, AggGroupNames } from 'src/plugins/data/public'; +import { + DefaultEditorAggParams as PureDefaultEditorAggParams, + DefaultEditorAggParamsProps, +} from './agg_params'; +import { KibanaContextProvider } from '../../../../../plugins/kibana_react/public'; +import { dataPluginMock } from '../../../../../plugins/data/public/mocks'; const mockEditorConfig = { useNormalizedEsInterval: { hidden: false, fixedValue: false }, @@ -34,8 +38,12 @@ const mockEditorConfig = { timeBase: '1m', }, }; +const DefaultEditorAggParams = (props: DefaultEditorAggParamsProps) => ( + + + +); -jest.mock('ui/new_platform'); jest.mock('./utils', () => ({ getEditorConfig: jest.fn(() => mockEditorConfig), })); @@ -109,12 +117,6 @@ describe('DefaultEditorAggParams component', () => { }; }); - it('should init with the default set of params', () => { - const comp = shallow(); - - expect(comp).toMatchSnapshot(); - }); - it('should reset the validity to true when destroyed', () => { const comp = mount(); diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/agg_params.tsx b/src/legacy/core_plugins/vis_default_editor/public/components/agg_params.tsx index 510c21af95da1..b1555b76500d0 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/agg_params.tsx +++ b/src/legacy/core_plugins/vis_default_editor/public/components/agg_params.tsx @@ -22,8 +22,7 @@ import { EuiForm, EuiAccordion, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import useUnmount from 'react-use/lib/useUnmount'; -import { IndexPattern } from 'src/plugins/data/public'; -import { IAggConfig, AggGroupNames } from '../legacy_imports'; +import { IAggConfig, IndexPattern, AggGroupNames } from '../../../../../plugins/data/public'; import { DefaultEditorAggSelect } from './agg_select'; import { DefaultEditorAggParam } from './agg_param'; @@ -41,6 +40,8 @@ import { import { DefaultEditorCommonProps } from './agg_common_props'; import { EditorParamConfig, TimeIntervalParam, FixedParam, getEditorConfig } from './utils'; import { Schema, getSchemaByName } from '../schemas'; +import { useKibana } from '../../../../../plugins/kibana_react/public'; +import { VisDefaultEditorKibanaServices } from '../types'; const FIXED_VALUE_PROP = 'fixedValue'; const DEFAULT_PROP = 'default'; @@ -83,18 +84,24 @@ function DefaultEditorAggParams({ allowedAggs = [], hideCustomLabel = false, }: DefaultEditorAggParamsProps) { - const schema = getSchemaByName(schemas, agg.schema); - const { title } = schema; - const aggFilter = [...allowedAggs, ...(schema.aggFilter || [])]; + const schema = useMemo(() => getSchemaByName(schemas, agg.schema), [agg.schema, schemas]); + const aggFilter = useMemo(() => [...allowedAggs, ...(schema.aggFilter || [])], [ + allowedAggs, + schema.aggFilter, + ]); + const { services } = useKibana(); + const aggTypes = useMemo(() => services.data.search.aggs.types.getAll(), [ + services.data.search.aggs.types, + ]); const groupedAggTypeOptions = useMemo( - () => getAggTypeOptions(agg, indexPattern, groupName, aggFilter), - [agg, indexPattern, groupName, aggFilter] + () => getAggTypeOptions(aggTypes, agg, indexPattern, groupName, aggFilter), + [aggTypes, agg, indexPattern, groupName, aggFilter] ); const error = aggIsTooLow ? i18n.translate('visDefaultEditor.aggParams.errors.aggWrongRunOrderErrorMessage', { defaultMessage: '"{schema}" aggs must run before all other buckets!', - values: { schema: title }, + values: { schema: schema.title }, }) : ''; const aggTypeName = agg.type?.name; @@ -105,8 +112,20 @@ function DefaultEditorAggParams({ fieldName, ]); const params = useMemo( - () => getAggParamsToRender({ agg, editorConfig, metricAggs, state, schemas, hideCustomLabel }), - [agg, editorConfig, metricAggs, state, schemas, hideCustomLabel] + () => + getAggParamsToRender( + { agg, editorConfig, metricAggs, state, schemas, hideCustomLabel }, + services.data.search.__LEGACY.aggTypeFieldFilters + ), + [ + agg, + editorConfig, + metricAggs, + state, + schemas, + hideCustomLabel, + services.data.search.__LEGACY.aggTypeFieldFilters, + ] ); const allParams = [...params.basic, ...params.advanced]; const [paramsState, onChangeParamsState] = useReducer( diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/agg_params_helper.test.ts b/src/legacy/core_plugins/vis_default_editor/public/components/agg_params_helper.test.ts index 047467750794b..f2ebbdc87a60a 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/agg_params_helper.test.ts +++ b/src/legacy/core_plugins/vis_default_editor/public/components/agg_params_helper.test.ts @@ -17,9 +17,15 @@ * under the License. */ -import { IndexPattern } from 'src/plugins/data/public'; +import { + AggGroupNames, + BUCKET_TYPES, + IAggConfig, + IAggType, + IndexPattern, + IndexPatternField, +} from 'src/plugins/data/public'; import { VisState } from 'src/legacy/core_plugins/visualizations/public'; -import { IAggConfig, IAggType, AggGroupNames, BUCKET_TYPES } from '../legacy_imports'; import { getAggParamsToRender, getAggTypeOptions, @@ -33,7 +39,11 @@ jest.mock('../utils', () => ({ groupAndSortBy: jest.fn(() => ['indexedFields']), })); -jest.mock('ui/new_platform'); +const mockFilter: any = { + filter(fields: IndexPatternField[]): IndexPatternField[] { + return fields; + }, +}; describe('DefaultEditorAggParams helpers', () => { describe('getAggParamsToRender', () => { @@ -62,14 +72,20 @@ describe('DefaultEditorAggParams helpers', () => { }, schema: 'metric', } as IAggConfig; - const params = getAggParamsToRender({ agg, editorConfig, metricAggs, state, schemas }); + const params = getAggParamsToRender( + { agg, editorConfig, metricAggs, state, schemas }, + mockFilter + ); expect(params).toEqual(emptyParams); }); it('should not create any param if there is no agg type', () => { agg = { schema: 'metric' } as IAggConfig; - const params = getAggParamsToRender({ agg, editorConfig, metricAggs, state, schemas }); + const params = getAggParamsToRender( + { agg, editorConfig, metricAggs, state, schemas }, + mockFilter + ); expect(params).toEqual(emptyParams); }); @@ -85,7 +101,10 @@ describe('DefaultEditorAggParams helpers', () => { hidden: true, }, }; - const params = getAggParamsToRender({ agg, editorConfig, metricAggs, state, schemas }); + const params = getAggParamsToRender( + { agg, editorConfig, metricAggs, state, schemas }, + mockFilter + ); expect(params).toEqual(emptyParams); }); @@ -97,7 +116,10 @@ describe('DefaultEditorAggParams helpers', () => { }, schema: 'metric2', } as any) as IAggConfig; - const params = getAggParamsToRender({ agg, editorConfig, metricAggs, state, schemas }); + const params = getAggParamsToRender( + { agg, editorConfig, metricAggs, state, schemas }, + mockFilter + ); expect(params).toEqual(emptyParams); }); @@ -136,7 +158,10 @@ describe('DefaultEditorAggParams helpers', () => { field: 'field', }, } as any) as IAggConfig; - const params = getAggParamsToRender({ agg, editorConfig, metricAggs, state, schemas }); + const params = getAggParamsToRender( + { agg, editorConfig, metricAggs, state, schemas }, + mockFilter + ); expect(params).toEqual({ basic: [ @@ -172,7 +197,13 @@ describe('DefaultEditorAggParams helpers', () => { describe('getAggTypeOptions', () => { it('should return agg type options grouped by subtype', () => { const indexPattern = {} as IndexPattern; - const aggs = getAggTypeOptions({} as IAggConfig, indexPattern, 'metrics', []); + const aggs = getAggTypeOptions( + { metrics: [] }, + {} as IAggConfig, + indexPattern, + 'metrics', + [] + ); expect(aggs).toEqual(['indexedFields']); }); diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/agg_params_helper.ts b/src/legacy/core_plugins/vis_default_editor/public/components/agg_params_helper.ts index 520ff6ffc5ff5..e07bf81697579 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/agg_params_helper.ts +++ b/src/legacy/core_plugins/vis_default_editor/public/components/agg_params_helper.ts @@ -19,23 +19,23 @@ import { get, isEmpty } from 'lodash'; -import { IndexPattern, IndexPatternField } from 'src/plugins/data/public'; -import { VisState } from 'src/legacy/core_plugins/visualizations/public'; -import { groupAndSortBy, ComboBoxGroupedOptions } from '../utils'; -import { AggTypeState, AggParamsState } from './agg_params_state'; -import { AggParamEditorProps } from './agg_param_props'; -import { aggParamsMap } from './agg_params_map'; import { - aggTypeFilters, - aggTypeFieldFilters, - aggTypes, + AggTypeFieldFilters, IAggConfig, AggParam, IFieldParamType, IAggType, -} from '../legacy_imports'; + IndexPattern, + IndexPatternField, +} from 'src/plugins/data/public'; +import { VisState } from 'src/legacy/core_plugins/visualizations/public'; +import { groupAndSortBy, ComboBoxGroupedOptions } from '../utils'; +import { AggTypeState, AggParamsState } from './agg_params_state'; +import { AggParamEditorProps } from './agg_param_props'; +import { aggParamsMap } from './agg_params_map'; import { EditorConfig } from './utils'; import { Schema, getSchemaByName } from '../schemas'; +import { search } from '../../../../../plugins/data/public'; interface ParamInstanceBase { agg: IAggConfig; @@ -53,14 +53,10 @@ export interface ParamInstance extends ParamInstanceBase { value: unknown; } -function getAggParamsToRender({ - agg, - editorConfig, - metricAggs, - state, - schemas, - hideCustomLabel, -}: ParamInstanceBase) { +function getAggParamsToRender( + { agg, editorConfig, metricAggs, state, schemas, hideCustomLabel }: ParamInstanceBase, + aggTypeFieldFilters: AggTypeFieldFilters +) { const params = { basic: [] as ParamInstance[], advanced: [] as ParamInstance[], @@ -136,13 +132,14 @@ function getAggParamsToRender({ } function getAggTypeOptions( + aggTypes: any, agg: IAggConfig, indexPattern: IndexPattern, groupName: string, allowedAggs: string[] ): ComboBoxGroupedOptions { - const aggTypeOptions = aggTypeFilters.filter( - (aggTypes as any)[groupName], + const aggTypeOptions = search.aggs.aggTypeFilters.filter( + aggTypes[groupName], indexPattern, agg, allowedAggs diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/agg_params_map.ts b/src/legacy/core_plugins/vis_default_editor/public/components/agg_params_map.ts index 7caa775dd4fa4..4517313b6fd6e 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/agg_params_map.ts +++ b/src/legacy/core_plugins/vis_default_editor/public/components/agg_params_map.ts @@ -22,11 +22,12 @@ import { AggGroupNames, BUCKET_TYPES, METRIC_TYPES, - siblingPipelineType, - parentPipelineType, -} from '../legacy_imports'; + search, +} from '../../../../../plugins/data/public'; import { wrapWithInlineComp } from './controls/utils'; +const { siblingPipelineType, parentPipelineType } = search.aggs; + const buckets = { [BUCKET_TYPES.DATE_HISTOGRAM]: { scaleMetricValues: controls.ScaleMetricsParamEditor, diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/agg_select.tsx b/src/legacy/core_plugins/vis_default_editor/public/components/agg_select.tsx index 4d969a2d8ec6c..7ee432946f3c8 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/agg_select.tsx +++ b/src/legacy/core_plugins/vis_default_editor/public/components/agg_select.tsx @@ -23,9 +23,8 @@ import { EuiComboBox, EuiComboBoxOptionOption, EuiFormRow, EuiLink, EuiText } fr import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { IndexPattern } from 'src/plugins/data/public'; +import { IAggType, IndexPattern } from 'src/plugins/data/public'; import { useKibana } from '../../../../../plugins/kibana_react/public'; -import { IAggType } from '../legacy_imports'; import { ComboBoxGroupedOptions } from '../utils'; import { AGG_TYPE_ACTION_KEYS, AggTypeAction } from './agg_params_state'; diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/agg_control_props.tsx b/src/legacy/core_plugins/vis_default_editor/public/components/controls/agg_control_props.tsx index 7f04b851902de..98540d3414f2d 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/agg_control_props.tsx +++ b/src/legacy/core_plugins/vis_default_editor/public/components/controls/agg_control_props.tsx @@ -18,7 +18,7 @@ */ import { VisParams } from 'src/legacy/core_plugins/visualizations/public'; -import { IAggConfig } from '../../legacy_imports'; +import { IAggConfig } from 'src/plugins/data/public'; import { DefaultEditorAggCommonProps } from '../agg_common_props'; export interface AggControlProps { diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/agg_utils.test.tsx b/src/legacy/core_plugins/vis_default_editor/public/components/controls/agg_utils.test.tsx index 0b847e3747b30..0c1e93bc1e646 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/agg_utils.test.tsx +++ b/src/legacy/core_plugins/vis_default_editor/public/components/controls/agg_utils.test.tsx @@ -20,7 +20,7 @@ import React, { FunctionComponent } from 'react'; import { mount, ReactWrapper } from 'enzyme'; -import { IAggConfig } from '../../legacy_imports'; +import { IAggConfig } from 'src/plugins/data/public'; import { safeMakeLabel, useAvailableOptions, diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/components/mask_list.tsx b/src/legacy/core_plugins/vis_default_editor/public/components/controls/components/mask_list.tsx index 625b09b05d28f..f6edecbbcbd70 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/components/mask_list.tsx +++ b/src/legacy/core_plugins/vis_default_editor/public/components/controls/components/mask_list.tsx @@ -22,7 +22,7 @@ import { EuiFieldText, EuiFlexItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { InputList, InputListConfig, InputObject, InputModel, InputItem } from './input_list'; -import { CidrMask } from '../../../legacy_imports'; +import { search } from '../../../../../../../plugins/data/public'; const EMPTY_STRING = ''; @@ -47,7 +47,7 @@ function MaskList({ showValidation, onBlur, ...rest }: MaskListProps) { defaultValue: { mask: { model: '0.0.0.0/1', value: '0.0.0.0/1', isInvalid: false }, }, - validateClass: CidrMask, + validateClass: search.aggs.CidrMask, getModelValue: (item: MaskObject = {}) => ({ mask: { model: item.mask || EMPTY_STRING, diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/field.test.tsx b/src/legacy/core_plugins/vis_default_editor/public/components/controls/field.test.tsx index 186738d0f551c..1043431475494 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/field.test.tsx +++ b/src/legacy/core_plugins/vis_default_editor/public/components/controls/field.test.tsx @@ -22,11 +22,10 @@ import { act } from 'react-dom/test-utils'; import { mount, shallow, ReactWrapper } from 'enzyme'; import { EuiComboBoxProps, EuiComboBox } from '@elastic/eui'; -import { IndexPatternField } from 'src/plugins/data/public'; +import { IAggConfig, IndexPatternField } from 'src/plugins/data/public'; import { VisState } from 'src/legacy/core_plugins/visualizations/public'; import { ComboBoxGroupedOptions } from '../../utils'; import { FieldParamEditor, FieldParamEditorProps } from './field'; -import { IAggConfig } from '../../legacy_imports'; function callComboBoxOnChange(comp: ReactWrapper, value: any = []) { const comboBoxProps = comp.find(EuiComboBox).props() as EuiComboBoxProps; diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/field.tsx b/src/legacy/core_plugins/vis_default_editor/public/components/controls/field.tsx index 0ec00ab6f20f0..59642ae4c25f7 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/field.tsx +++ b/src/legacy/core_plugins/vis_default_editor/public/components/controls/field.tsx @@ -23,8 +23,7 @@ import React, { useEffect, useState, useCallback } from 'react'; import { EuiComboBox, EuiComboBoxOptionOption, EuiFormRow } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { IndexPatternField } from 'src/plugins/data/public'; -import { AggParam, IAggConfig, IFieldParamType } from '../../legacy_imports'; +import { AggParam, IAggConfig, IFieldParamType, IndexPatternField } from 'src/plugins/data/public'; import { formatListAsProse, parseCommaSeparatedList, useValidation } from './utils'; import { AggParamEditorProps } from '../agg_param_props'; import { ComboBoxGroupedOptions } from '../../utils'; diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/filter.tsx b/src/legacy/core_plugins/vis_default_editor/public/components/controls/filter.tsx index 3622b27bad403..e2e7c2895093e 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/filter.tsx +++ b/src/legacy/core_plugins/vis_default_editor/public/components/controls/filter.tsx @@ -21,8 +21,7 @@ import React, { useState } from 'react'; import { EuiForm, EuiButtonIcon, EuiFieldText, EuiFormRow, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { Query, QueryStringInput } from '../../../../../../plugins/data/public'; -import { IAggConfig } from '../../legacy_imports'; +import { IAggConfig, Query, QueryStringInput } from '../../../../../../plugins/data/public'; interface FilterRowProps { id: string; diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/has_extended_bounds.tsx b/src/legacy/core_plugins/vis_default_editor/public/components/controls/has_extended_bounds.tsx index 416f925da8c1e..90b7cb03b7a5b 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/has_extended_bounds.tsx +++ b/src/legacy/core_plugins/vis_default_editor/public/components/controls/has_extended_bounds.tsx @@ -20,10 +20,12 @@ import React, { useEffect } from 'react'; import { i18n } from '@kbn/i18n'; +import { search } from '../../../../../../plugins/data/public'; import { SwitchParamEditor } from './switch'; -import { isType } from '../../legacy_imports'; import { AggParamEditorProps } from '../agg_param_props'; +const { isType } = search.aggs; + function HasExtendedBoundsParamEditor(props: AggParamEditorProps) { useEffect(() => { props.setValue(props.value && props.agg.params.min_doc_count); diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/metric_agg.test.tsx b/src/legacy/core_plugins/vis_default_editor/public/components/controls/metric_agg.test.tsx index cf7af1aa5cb3a..c53e7a8beb831 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/metric_agg.test.tsx +++ b/src/legacy/core_plugins/vis_default_editor/public/components/controls/metric_agg.test.tsx @@ -20,7 +20,7 @@ import React from 'react'; import { mount, shallow } from 'enzyme'; -import { IAggConfig } from '../../legacy_imports'; +import { IAggConfig } from 'src/plugins/data/public'; import { DEFAULT_OPTIONS, aggFilter, MetricAggParamEditor } from './metric_agg'; jest.mock('./utils', () => ({ diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/missing_bucket.tsx b/src/legacy/core_plugins/vis_default_editor/public/components/controls/missing_bucket.tsx index 8d22ab283f3a1..7010f0d53e569 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/missing_bucket.tsx +++ b/src/legacy/core_plugins/vis_default_editor/public/components/controls/missing_bucket.tsx @@ -21,11 +21,11 @@ import React, { useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import { SwitchParamEditor } from './switch'; -import { isStringType } from '../../legacy_imports'; +import { search } from '../../../../../../plugins/data/public'; import { AggParamEditorProps } from '../agg_param_props'; function MissingBucketParamEditor(props: AggParamEditorProps) { - const fieldTypeIsNotString = !isStringType(props.agg); + const fieldTypeIsNotString = !search.aggs.isStringType(props.agg); useEffect(() => { if (fieldTypeIsNotString) { diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/order.tsx b/src/legacy/core_plugins/vis_default_editor/public/components/controls/order.tsx index f40143251e46a..8f63662d928c1 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/order.tsx +++ b/src/legacy/core_plugins/vis_default_editor/public/components/controls/order.tsx @@ -21,7 +21,7 @@ import React, { useEffect } from 'react'; import { EuiFormRow, EuiSelect } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { OptionedValueProp, OptionedParamEditorProps } from '../../legacy_imports'; +import { OptionedValueProp, OptionedParamEditorProps } from 'src/plugins/data/public'; import { AggParamEditorProps } from '../agg_param_props'; function OrderParamEditor({ diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/order_agg.test.tsx b/src/legacy/core_plugins/vis_default_editor/public/components/controls/order_agg.test.tsx index 01f5ed9b6a2f1..4c843791153b0 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/order_agg.test.tsx +++ b/src/legacy/core_plugins/vis_default_editor/public/components/controls/order_agg.test.tsx @@ -21,8 +21,6 @@ import React from 'react'; import { mount } from 'enzyme'; import { OrderByParamEditor } from './order_by'; -jest.mock('ui/new_platform'); - describe('OrderAggParamEditor component', () => { let setValue: jest.Mock; let setValidity: jest.Mock; diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/order_agg.tsx b/src/legacy/core_plugins/vis_default_editor/public/components/controls/order_agg.tsx index 8c020c668b3c6..41672bc192fab 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/order_agg.tsx +++ b/src/legacy/core_plugins/vis_default_editor/public/components/controls/order_agg.tsx @@ -20,7 +20,7 @@ import React, { useEffect } from 'react'; import { EuiSpacer } from '@elastic/eui'; -import { AggParamType, IAggConfig, AggGroupNames } from '../../legacy_imports'; +import { AggParamType, IAggConfig, AggGroupNames } from '../../../../../../plugins/data/public'; import { useSubAggParamsHandlers } from './utils'; import { AggParamEditorProps } from '../agg_param_props'; import { DefaultEditorAggParams } from '../agg_params'; diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/order_by.tsx b/src/legacy/core_plugins/vis_default_editor/public/components/controls/order_by.tsx index c0391358ec6e2..9f1aaa54a8ca3 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/order_by.tsx +++ b/src/legacy/core_plugins/vis_default_editor/public/components/controls/order_by.tsx @@ -28,8 +28,9 @@ import { useValidation, } from './utils'; import { AggParamEditorProps } from '../agg_param_props'; -import { termsAggFilter } from '../../legacy_imports'; +import { search } from '../../../../../../plugins/data/public'; +const { termsAggFilter } = search.aggs; const DEFAULT_VALUE = '_key'; const DEFAULT_OPTIONS = [ { diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/percentiles.test.tsx b/src/legacy/core_plugins/vis_default_editor/public/components/controls/percentiles.test.tsx index 0eaf9bcc987c1..76eb12af8c4e2 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/percentiles.test.tsx +++ b/src/legacy/core_plugins/vis_default_editor/public/components/controls/percentiles.test.tsx @@ -19,7 +19,7 @@ import React from 'react'; import { AggParamEditorProps } from '../agg_param_props'; -import { IAggConfig } from '../../legacy_imports'; +import { IAggConfig } from 'src/plugins/data/public'; import { VisState } from 'src/legacy/core_plugins/visualizations/public'; import { mount } from 'enzyme'; import { PercentilesEditor } from './percentiles'; diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/sub_agg.tsx b/src/legacy/core_plugins/vis_default_editor/public/components/controls/sub_agg.tsx index 5bc94bd4af226..c9f53a68b3e83 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/sub_agg.tsx +++ b/src/legacy/core_plugins/vis_default_editor/public/components/controls/sub_agg.tsx @@ -20,7 +20,7 @@ import React, { useEffect } from 'react'; import { EuiSpacer } from '@elastic/eui'; -import { AggParamType, IAggConfig, AggGroupNames } from '../../legacy_imports'; +import { AggParamType, IAggConfig, AggGroupNames } from '../../../../../../plugins/data/public'; import { useSubAggParamsHandlers } from './utils'; import { AggParamEditorProps } from '../agg_param_props'; import { DefaultEditorAggParams } from '../agg_params'; diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/sub_metric.tsx b/src/legacy/core_plugins/vis_default_editor/public/components/controls/sub_metric.tsx index 9d48b1c964a27..ead3f8bb00623 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/sub_metric.tsx +++ b/src/legacy/core_plugins/vis_default_editor/public/components/controls/sub_metric.tsx @@ -21,7 +21,7 @@ import React, { useEffect } from 'react'; import { EuiFormLabel, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { AggParamType, IAggConfig, AggGroupNames } from '../../legacy_imports'; +import { AggParamType, IAggConfig, AggGroupNames } from '../../../../../../plugins/data/public'; import { useSubAggParamsHandlers } from './utils'; import { AggParamEditorProps } from '../agg_param_props'; import { DefaultEditorAggParams } from '../agg_params'; diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/test_utils.ts b/src/legacy/core_plugins/vis_default_editor/public/components/controls/test_utils.ts index 8a21114999cd6..b816e61cce355 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/test_utils.ts +++ b/src/legacy/core_plugins/vis_default_editor/public/components/controls/test_utils.ts @@ -18,7 +18,7 @@ */ import { VisState } from 'src/legacy/core_plugins/visualizations/public'; -import { IAggConfig, AggParam } from '../../legacy_imports'; +import { IAggConfig, AggParam } from 'src/plugins/data/public'; import { EditorConfig } from '../utils'; export const aggParamCommonPropsMock = { diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/time_interval.tsx b/src/legacy/core_plugins/vis_default_editor/public/components/controls/time_interval.tsx index ee3666b2ed441..de0059f5467ad 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/time_interval.tsx +++ b/src/legacy/core_plugins/vis_default_editor/public/components/controls/time_interval.tsx @@ -23,7 +23,7 @@ import { EuiFormRow, EuiIconTip, EuiComboBox, EuiComboBoxOptionOption } from '@e import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { isValidInterval, AggParamOption } from '../../legacy_imports'; +import { search, AggParamOption } from '../../../../../../plugins/data/public'; import { AggParamEditorProps } from '../agg_param_props'; interface ComboBoxOption extends EuiComboBoxOptionOption { @@ -59,7 +59,7 @@ function TimeIntervalParamEditor({ if (value) { definedOption = find(options, { key: value }); selectedOptions = definedOption ? [definedOption] : [{ label: value, key: 'custom' }]; - isValid = !!(definedOption || isValidInterval(value, timeBase)); + isValid = !!(definedOption || search.aggs.isValidInterval(value, timeBase)); } const interval = get(agg, 'buckets.getInterval') && (agg as any).buckets.getInterval(); @@ -100,7 +100,7 @@ function TimeIntervalParamEditor({ const normalizedCustomValue = customValue.trim(); setValue(normalizedCustomValue); - if (normalizedCustomValue && isValidInterval(normalizedCustomValue, timeBase)) { + if (normalizedCustomValue && search.aggs.isValidInterval(normalizedCustomValue, timeBase)) { agg.write(); } }; diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/top_aggregate.test.tsx b/src/legacy/core_plugins/vis_default_editor/public/components/controls/top_aggregate.test.tsx index 4ce0712040bd5..74dab1a3b551a 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/top_aggregate.test.tsx +++ b/src/legacy/core_plugins/vis_default_editor/public/components/controls/top_aggregate.test.tsx @@ -25,7 +25,7 @@ import { TopAggregateParamEditorProps, } from './top_aggregate'; import { aggParamCommonPropsMock } from './test_utils'; -import { IAggConfig } from '../../legacy_imports'; +import { IAggConfig } from 'src/plugins/data/public'; describe('TopAggregateParamEditor', () => { let agg: IAggConfig; diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/top_aggregate.tsx b/src/legacy/core_plugins/vis_default_editor/public/components/controls/top_aggregate.tsx index 346dfc0156f07..bab20d18c8fc0 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/top_aggregate.tsx +++ b/src/legacy/core_plugins/vis_default_editor/public/components/controls/top_aggregate.tsx @@ -28,7 +28,7 @@ import { OptionedValueProp, OptionedParamEditorProps, OptionedParamType, -} from '../../legacy_imports'; +} from 'src/plugins/data/public'; import { AggParamEditorProps } from '../agg_param_props'; export interface AggregateValueProp extends OptionedValueProp { diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/utils/agg_utils.ts b/src/legacy/core_plugins/vis_default_editor/public/components/controls/utils/agg_utils.ts index 8aeae488942cd..f4c0814748ebc 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/utils/agg_utils.ts +++ b/src/legacy/core_plugins/vis_default_editor/public/components/controls/utils/agg_utils.ts @@ -20,7 +20,7 @@ import { useEffect, useCallback, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; -import { IAggConfig } from '../../../legacy_imports'; +import { IAggConfig } from 'src/plugins/data/public'; type AggFilter = string[]; diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/utils/use_handlers.ts b/src/legacy/core_plugins/vis_default_editor/public/components/controls/utils/use_handlers.ts index c7816d5a9d305..4dadef79b1204 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/utils/use_handlers.ts +++ b/src/legacy/core_plugins/vis_default_editor/public/components/controls/utils/use_handlers.ts @@ -19,7 +19,7 @@ import { useCallback } from 'react'; -import { IAggConfig, AggParamType } from '../../../legacy_imports'; +import { IAggConfig, AggParamType } from 'src/plugins/data/public'; type SetValue = (value?: IAggConfig) => void; diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/data_tab.tsx b/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/data_tab.tsx index 1c1f9d57d8b90..6f92c27e90ec1 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/data_tab.tsx +++ b/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/data_tab.tsx @@ -23,11 +23,11 @@ import { EuiSpacer } from '@elastic/eui'; import { VisState } from 'src/legacy/core_plugins/visualizations/public'; import { - IAggConfig, AggGroupNames, - parentPipelineType, + IAggConfig, IMetricAggType, -} from '../../legacy_imports'; + search, +} from '../../../../../../plugins/data/public'; import { DefaultEditorAggGroup } from '../agg_group'; import { EditorAction, @@ -67,7 +67,7 @@ function DefaultEditorDataTab({ () => findLast( metricAggs, - ({ type }: { type: IMetricAggType }) => type.subtype === parentPipelineType + ({ type }: { type: IMetricAggType }) => type.subtype === search.aggs.parentPipelineType ), [metricAggs] ); 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 1efd8dae8178b..2508ef3a55537 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 @@ -23,7 +23,6 @@ import { i18n } from '@kbn/i18n'; import { keyCodes, EuiButtonIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { Vis } from 'src/legacy/core_plugins/visualizations/public'; -import { AggGroupNames } from '../../legacy_imports'; import { DefaultEditorNavBar, OptionTab } from './navbar'; import { DefaultEditorControls } from './controls'; import { setStateParamValue, useEditorReducer, useEditorFormState, discardChanges } from './state'; @@ -31,6 +30,7 @@ import { DefaultEditorAggCommonProps } from '../agg_common_props'; import { SidebarTitle } from './sidebar_title'; import { PersistedState } from '../../../../../../plugins/visualizations/public'; import { SavedSearch } from '../../../../../../plugins/discover/public'; +import { AggGroupNames } from '../../../../../../plugins/data/public'; import { getSchemasByGroup } from '../../schemas'; interface DefaultEditorSideBarProps { diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/state/actions.ts b/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/state/actions.ts index f9915bedc8878..e3577218b7e25 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/state/actions.ts +++ b/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/state/actions.ts @@ -18,7 +18,7 @@ */ import { Vis, VisParams } from 'src/legacy/core_plugins/visualizations/public'; -import { IAggConfig } from '../../../legacy_imports'; +import { IAggConfig } from 'src/plugins/data/public'; import { EditorStateActionTypes } from './constants'; import { Schema } from '../../../schemas'; diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/state/index.ts b/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/state/index.ts index df5ba3f6121c7..6383ac866dcfc 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/state/index.ts +++ b/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/state/index.ts @@ -21,15 +21,22 @@ import { useEffect, useReducer, useCallback } from 'react'; import { isEqual } from 'lodash'; import { Vis, VisState, VisParams } from 'src/legacy/core_plugins/visualizations/public'; -import { editorStateReducer, initEditorState } from './reducers'; +import { createEditorStateReducer, initEditorState } from './reducers'; import { EditorStateActionTypes } from './constants'; import { EditorAction, updateStateParams } from './actions'; +import { useKibana } from '../../../../../../../plugins/kibana_react/public'; +import { VisDefaultEditorKibanaServices } from '../../../types'; export * from './editor_form_state'; export * from './actions'; export function useEditorReducer(vis: Vis): [VisState, React.Dispatch] { - const [state, dispatch] = useReducer(editorStateReducer, vis, initEditorState); + const { services } = useKibana(); + const [state, dispatch] = useReducer( + createEditorStateReducer(services.data.search), + vis, + initEditorState + ); useEffect(() => { const handleVisUpdate = (params: VisParams) => { diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/state/reducers.ts b/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/state/reducers.ts index 73675e75cbe36..67220fd9fd91b 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/state/reducers.ts +++ b/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/state/reducers.ts @@ -20,7 +20,7 @@ import { cloneDeep } from 'lodash'; import { Vis, VisState } from 'src/legacy/core_plugins/visualizations/public'; -import { createAggConfigs, AggGroupNames } from '../../../legacy_imports'; +import { AggGroupNames, DataPublicPluginStart } from '../../../../../../../plugins/data/public'; import { EditorStateActionTypes } from './constants'; import { getEnabledMetricAggsCount } from '../../agg_group_helper'; import { EditorAction } from './actions'; @@ -29,7 +29,9 @@ function initEditorState(vis: Vis) { return vis.copyCurrentState(true); } -function editorStateReducer(state: VisState, action: EditorAction): VisState { +const createEditorStateReducer = ({ + aggs: { createAggConfigs }, +}: DataPublicPluginStart['search']) => (state: VisState, action: EditorAction): VisState => { switch (action.type) { case EditorStateActionTypes.ADD_NEW_AGG: { const { schema } = action.payload; @@ -181,6 +183,6 @@ function editorStateReducer(state: VisState, action: EditorAction): VisState { }; } } -} +}; -export { editorStateReducer, initEditorState }; +export { createEditorStateReducer, initEditorState }; diff --git a/src/legacy/core_plugins/vis_default_editor/public/legacy_imports.ts b/src/legacy/core_plugins/vis_default_editor/public/legacy_imports.ts deleted file mode 100644 index 50028d8c970f4..0000000000000 --- a/src/legacy/core_plugins/vis_default_editor/public/legacy_imports.ts +++ /dev/null @@ -1,47 +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. - */ - -/* `ui/agg_types` dependencies */ -export { BUCKET_TYPES, METRIC_TYPES } from '../../../../plugins/data/public'; -export { - AggGroupNames, - aggGroupNamesMap, - AggParam, - AggParamType, - AggType, - aggTypes, - createAggConfigs, - FieldParamType, - IAggConfig, - IAggConfigs, - IAggGroupNames, - IAggType, - IFieldParamType, - termsAggFilter, -} from 'ui/agg_types'; -export { aggTypeFilters, propFilter } from 'ui/agg_types'; -export { aggTypeFieldFilters } from 'ui/agg_types'; -export { MetricAggType, IMetricAggType } from 'ui/agg_types'; -export { parentPipelineType } from 'ui/agg_types'; -export { siblingPipelineType } from 'ui/agg_types'; -export { isType, isStringType } from 'ui/agg_types'; -export { OptionedValueProp, OptionedParamEditorProps, OptionedParamType } from 'ui/agg_types'; -export { isValidInterval } from 'ui/agg_types'; -export { AggParamOption } from 'ui/agg_types'; -export { CidrMask } from 'ui/agg_types'; diff --git a/src/legacy/core_plugins/vis_default_editor/public/types.ts b/src/legacy/core_plugins/vis_default_editor/public/types.ts new file mode 100644 index 0000000000000..22fc24005994d --- /dev/null +++ b/src/legacy/core_plugins/vis_default_editor/public/types.ts @@ -0,0 +1,24 @@ +/* + * 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 { DataPublicPluginStart } from 'src/plugins/data/public'; + +export interface VisDefaultEditorKibanaServices { + data: DataPublicPluginStart; +} diff --git a/src/legacy/core_plugins/vis_default_editor/public/utils.test.ts b/src/legacy/core_plugins/vis_default_editor/public/utils.test.ts index b050979b7b338..f3912450ba670 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/utils.test.ts +++ b/src/legacy/core_plugins/vis_default_editor/public/utils.test.ts @@ -18,9 +18,7 @@ */ import { groupAndSortBy } from './utils'; -import { AggGroupNames } from './legacy_imports'; - -jest.mock('ui/new_platform'); +import { AggGroupNames } from 'src/plugins/data/public'; const aggs = [ { diff --git a/src/legacy/core_plugins/vis_default_editor/public/vis_options_props.tsx b/src/legacy/core_plugins/vis_default_editor/public/vis_options_props.tsx index 18fbba1b039b5..2e8f20946c73a 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/vis_options_props.tsx +++ b/src/legacy/core_plugins/vis_default_editor/public/vis_options_props.tsx @@ -17,8 +17,8 @@ * under the License. */ +import { IAggConfigs } from 'src/plugins/data/public'; import { PersistedState } from '../../../../plugins/visualizations/public'; -import { IAggConfigs } from './legacy_imports'; import { Vis } from '../../visualizations/public'; export interface VisOptionsProps { diff --git a/src/legacy/core_plugins/vis_default_editor/public/vis_type_agg_filter.ts b/src/legacy/core_plugins/vis_default_editor/public/vis_type_agg_filter.ts index fcb06f73513b0..3ff212c43e6e8 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/vis_type_agg_filter.ts +++ b/src/legacy/core_plugins/vis_default_editor/public/vis_type_agg_filter.ts @@ -16,9 +16,9 @@ * specific language governing permissions and limitations * under the License. */ -import { IndexPattern } from 'src/plugins/data/public'; -import { IAggType, IAggConfig, aggTypeFilters, propFilter } from './legacy_imports'; +import { IAggType, IAggConfig, IndexPattern, search } from '../../../../plugins/data/public'; +const { aggTypeFilters, propFilter } = search.aggs; const filterByName = propFilter('name'); /** diff --git a/src/legacy/core_plugins/vis_type_metric/public/legacy_imports.ts b/src/legacy/core_plugins/vis_type_metric/public/legacy_imports.ts index b769030a04fb1..cd7a8e740d85d 100644 --- a/src/legacy/core_plugins/vis_type_metric/public/legacy_imports.ts +++ b/src/legacy/core_plugins/vis_type_metric/public/legacy_imports.ts @@ -18,4 +18,3 @@ */ export { getFormat } from 'ui/visualize/loader/pipeline_helpers/utilities'; -export { AggGroupNames, Schemas } from 'ui/agg_types'; diff --git a/src/legacy/core_plugins/vis_type_metric/public/metric_vis_fn.test.ts b/src/legacy/core_plugins/vis_type_metric/public/metric_vis_fn.test.ts index 3bddc94929cf5..22c32895d6803 100644 --- a/src/legacy/core_plugins/vis_type_metric/public/metric_vis_fn.test.ts +++ b/src/legacy/core_plugins/vis_type_metric/public/metric_vis_fn.test.ts @@ -23,21 +23,8 @@ import { functionWrapper } from '../../../../plugins/expressions/common/expressi jest.mock('ui/new_platform'); -jest.mock('../../vis_default_editor/public/legacy_imports', () => ({ - propFilter: jest.fn(), - AggGroupNames: { - Buckets: 'buckets', - Metrics: 'metrics', - }, - aggTypeFilters: { - addFilter: jest.fn(), - }, - BUCKET_TYPES: { - DATE_HISTOGRAM: 'date_histogram', - }, - METRIC_TYPES: { - TOP_HITS: 'top_hits', - }, +jest.mock('../../vis_default_editor/public', () => ({ + Schemas: class {}, })); describe('interpreter/functions#metric', () => { diff --git a/src/legacy/core_plugins/vis_type_metric/public/metric_vis_type.test.ts b/src/legacy/core_plugins/vis_type_metric/public/metric_vis_type.test.ts index 5813465cc3f00..cce5864aa50a1 100644 --- a/src/legacy/core_plugins/vis_type_metric/public/metric_vis_type.test.ts +++ b/src/legacy/core_plugins/vis_type_metric/public/metric_vis_type.test.ts @@ -36,21 +36,8 @@ import { createMetricVisTypeDefinition } from './metric_vis_type'; jest.mock('ui/new_platform'); -jest.mock('../../vis_default_editor/public/legacy_imports', () => ({ - propFilter: jest.fn(), - AggGroupNames: { - Buckets: 'buckets', - Metrics: 'metrics', - }, - aggTypeFilters: { - addFilter: jest.fn(), - }, - BUCKET_TYPES: { - DATE_HISTOGRAM: 'date_histogram', - }, - METRIC_TYPES: { - TOP_HITS: 'top_hits', - }, +jest.mock('../../vis_default_editor/public', () => ({ + Schemas: class {}, })); describe('metric_vis - createMetricVisTypeDefinition', () => { diff --git a/src/legacy/core_plugins/vis_type_metric/public/metric_vis_type.ts b/src/legacy/core_plugins/vis_type_metric/public/metric_vis_type.ts index 0b8d9b17659f4..f29164f7e540d 100644 --- a/src/legacy/core_plugins/vis_type_metric/public/metric_vis_type.ts +++ b/src/legacy/core_plugins/vis_type_metric/public/metric_vis_type.ts @@ -22,8 +22,9 @@ import { i18n } from '@kbn/i18n'; import { MetricVisComponent } from './components/metric_vis_component'; import { MetricVisOptions } from './components/metric_vis_options'; import { ColorModes } from '../../vis_type_vislib/public'; -import { Schemas, AggGroupNames } from './legacy_imports'; import { ColorSchemas, colorSchemas } from '../../../../plugins/charts/public'; +import { AggGroupNames } from '../../../../plugins/data/public'; +import { Schemas } from '../../vis_default_editor/public'; export const createMetricVisTypeDefinition = () => ({ name: 'metric', 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 91581923b05cb..8edef2ea16353 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 @@ -27,7 +27,8 @@ import { oneTermOneHistogramBucketWithTwoMetricsOneTopHitOneDerivative, } from 'fixtures/fake_hierarchical_data'; import sinon from 'sinon'; -import { tabifyAggResponse, npStart } from '../../legacy_imports'; +import { npStart } from '../../legacy_imports'; +import { search } from '../../../../../../plugins/data/public'; import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; import { round } from 'lodash'; import { tableVisTypeDefinition } from '../../table_vis_type'; @@ -39,6 +40,8 @@ import { getAngularModule } from '../../get_inner_angular'; import { initTableVisLegacyModule } from '../../table_vis_legacy_module'; import { tableVisResponseHandler } from '../../table_vis_response_handler'; +const { tabifyAggResponse } = search; + describe('Table Vis - AggTable Directive', function() { let $rootScope; let $compile; 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 4d62551dcf396..89900d2144030 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 @@ -21,13 +21,16 @@ import $ from 'jquery'; import ngMock from 'ng_mock'; import expect from '@kbn/expect'; import { metricOnly, threeTermBuckets } from 'fixtures/fake_hierarchical_data'; -import { tabifyAggResponse, npStart } from '../../legacy_imports'; +import { npStart } from '../../legacy_imports'; +import { search } from '../../../../../../plugins/data/public'; import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; import { getAngularModule } from '../../get_inner_angular'; import { initTableVisLegacyModule } from '../../table_vis_legacy_module'; import { tableVisResponseHandler } from '../../table_vis_response_handler'; import { start as visualizationsStart } from '../../../../visualizations/public/np_ready/public/legacy'; +const { tabifyAggResponse } = search; + describe('Table Vis - AggTableGroup Directive', function() { let $rootScope; let $compile; diff --git a/src/legacy/core_plugins/vis_type_table/public/components/table_vis_options.tsx b/src/legacy/core_plugins/vis_type_table/public/components/table_vis_options.tsx index 8cc0ca2456867..30a9526273166 100644 --- a/src/legacy/core_plugins/vis_type_table/public/components/table_vis_options.tsx +++ b/src/legacy/core_plugins/vis_type_table/public/components/table_vis_options.tsx @@ -24,11 +24,13 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { VisOptionsProps } from 'src/legacy/core_plugins/vis_default_editor/public'; -import { tabifyGetColumns } from '../legacy_imports'; +import { search } from '../../../../../plugins/data/public'; import { NumberInputOption, SwitchOption, SelectOption } from '../../../vis_type_vislib/public'; import { TableVisParams } from '../types'; import { totalAggregations } from './utils'; +const { tabifyGetColumns } = search; + function TableOptions({ aggs, stateParams, diff --git a/src/legacy/core_plugins/vis_type_table/public/get_inner_angular.ts b/src/legacy/core_plugins/vis_type_table/public/get_inner_angular.ts index 6fb5658d8e815..6208e358b4184 100644 --- a/src/legacy/core_plugins/vis_type_table/public/get_inner_angular.ts +++ b/src/legacy/core_plugins/vis_type_table/public/get_inner_angular.ts @@ -25,14 +25,14 @@ import 'angular-recursion'; import { i18nDirective, i18nFilter, I18nProvider } from '@kbn/i18n/angular'; import { CoreStart, LegacyCoreStart, IUiSettingsClient } from 'kibana/public'; import { - PrivateProvider, + initAngularBootstrap, PaginateDirectiveProvider, PaginateControlsDirectiveProvider, + PrivateProvider, watchMultiDecorator, KbnAccessibleClickProvider, configureAppAngularModule, -} from './legacy_imports'; -import { initAngularBootstrap } from '../../../../plugins/kibana_legacy/public'; +} from '../../../../plugins/kibana_legacy/public'; initAngularBootstrap(); 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 7b584f8069338..287b6c172ffd9 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 @@ -19,16 +19,3 @@ export { npSetup, npStart } from 'ui/new_platform'; export { getFormat } from 'ui/visualize/loader/pipeline_helpers/utilities'; -export { IAggConfig, AggGroupNames, Schemas } from 'ui/agg_types'; -// @ts-ignore -export { PaginateDirectiveProvider } from 'ui/directives/paginate'; -// @ts-ignore -export { PaginateControlsDirectiveProvider } from 'ui/directives/paginate'; -import { search } from '../../../../plugins/data/public'; -export const { tabifyAggResponse, tabifyGetColumns } = search; -export { - configureAppAngularModule, - KbnAccessibleClickProvider, - PrivateProvider, - watchMultiDecorator, -} from '../../../../plugins/kibana_legacy/public'; diff --git a/src/legacy/core_plugins/vis_type_table/public/table_vis_controller.test.ts b/src/legacy/core_plugins/vis_type_table/public/table_vis_controller.test.ts index 6d4e94c6292a6..327a47093f535 100644 --- a/src/legacy/core_plugins/vis_type_table/public/table_vis_controller.test.ts +++ b/src/legacy/core_plugins/vis_type_table/public/table_vis_controller.test.ts @@ -34,8 +34,13 @@ import { stubFields } from '../../../../plugins/data/public/stubs'; import { tableVisResponseHandler } from './table_vis_response_handler'; import { coreMock } from '../../../../core/public/mocks'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { createAggConfigs } from 'ui/agg_types'; -import { tabifyAggResponse, IAggConfig } from './legacy_imports'; +import { npStart } from './legacy_imports'; +import { IAggConfig, search } from '../../../../plugins/data/public'; + +// should be mocked once get rid of 'ui/new_platform' legacy imports +const { createAggConfigs } = npStart.plugins.data.search.aggs; + +const { tabifyAggResponse } = search; jest.mock('ui/new_platform'); jest.mock('../../../../plugins/kibana_legacy/public/angular/angular_config', () => ({ diff --git a/src/legacy/core_plugins/vis_type_table/public/table_vis_type.ts b/src/legacy/core_plugins/vis_type_table/public/table_vis_type.ts index 970bf1ba7ce64..e70b09904253f 100644 --- a/src/legacy/core_plugins/vis_type_table/public/table_vis_type.ts +++ b/src/legacy/core_plugins/vis_type_table/public/table_vis_type.ts @@ -18,7 +18,8 @@ */ import { i18n } from '@kbn/i18n'; -import { AggGroupNames, Schemas } from './legacy_imports'; +import { AggGroupNames } from '../../../../plugins/data/public'; +import { Schemas } from '../../vis_default_editor/public'; import { Vis } from '../../visualizations/public'; import { tableVisResponseHandler } from './table_vis_response_handler'; // @ts-ignore diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/legacy_imports.ts b/src/legacy/core_plugins/vis_type_tagcloud/public/legacy_imports.ts index 0d76bc5d8b68b..cd7a8e740d85d 100644 --- a/src/legacy/core_plugins/vis_type_tagcloud/public/legacy_imports.ts +++ b/src/legacy/core_plugins/vis_type_tagcloud/public/legacy_imports.ts @@ -17,5 +17,4 @@ * under the License. */ -export { Schemas } from 'ui/agg_types'; export { getFormat } from 'ui/visualize/loader/pipeline_helpers/utilities'; diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/tag_cloud_type.ts b/src/legacy/core_plugins/vis_type_tagcloud/public/tag_cloud_type.ts index 34d15287169c0..9a522fe6e648e 100644 --- a/src/legacy/core_plugins/vis_type_tagcloud/public/tag_cloud_type.ts +++ b/src/legacy/core_plugins/vis_type_tagcloud/public/tag_cloud_type.ts @@ -19,7 +19,7 @@ import { i18n } from '@kbn/i18n'; -import { Schemas } from './legacy_imports'; +import { Schemas } from '../../vis_default_editor/public'; import { Status } from '../../visualizations/public'; import { TagCloudOptions } from './components/tag_cloud_options'; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/area.ts b/src/legacy/core_plugins/vis_type_vislib/public/area.ts index 71027d7db5af8..e79555470298b 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/area.ts +++ b/src/legacy/core_plugins/vis_type_vislib/public/area.ts @@ -23,7 +23,8 @@ import { palettes } from '@elastic/eui/lib/services'; // @ts-ignore import { euiPaletteColorBlind } from '@elastic/eui/lib/services'; -import { Schemas, AggGroupNames } from './legacy_imports'; +import { AggGroupNames } from '../../../../plugins/data/public'; +import { Schemas } from '../../vis_default_editor/public'; import { Positions, ChartTypes, diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/gauge/style_panel.tsx b/src/legacy/core_plugins/vis_type_vislib/public/components/options/gauge/style_panel.tsx index 4c936c93a4c8a..9254c3c18347c 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/gauge/style_panel.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/components/options/gauge/style_panel.tsx @@ -24,7 +24,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { SelectOption } from '../../common'; import { GaugeOptionsInternalProps } from '.'; -import { AggGroupNames } from '../../../legacy_imports'; +import { AggGroupNames } from '../../../../../../../plugins/data/public'; function StylePanel({ aggs, setGaugeValue, stateParams, vis }: GaugeOptionsInternalProps) { const diasableAlignment = diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/index.test.tsx b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/index.test.tsx index f172a4344c940..032dd10cf11d2 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/index.test.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/index.test.tsx @@ -20,6 +20,7 @@ import React from 'react'; import { mount, shallow } from 'enzyme'; +import { IAggConfig, IAggType } from 'src/plugins/data/public'; import { MetricsAxisOptions } from './index'; import { BasicVislibParams, SeriesParam, ValueAxis } from '../../../types'; import { ValidationVisOptionsProps } from '../../common'; @@ -27,10 +28,8 @@ import { Positions } from '../../../utils/collections'; import { ValueAxesPanel } from './value_axes_panel'; import { CategoryAxisPanel } from './category_axis_panel'; import { ChartTypes } from '../../../utils/collections'; -import { IAggConfig, IAggType } from '../../../legacy_imports'; import { defaultValueAxisId, valueAxis, seriesParam, categoryAxis } from './mocks'; -jest.mock('ui/new_platform'); jest.mock('./series_panel', () => ({ SeriesPanel: () => 'SeriesPanel', })); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/index.tsx b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/index.tsx index 82b64e4185ed2..a6f4a967d9c76 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/index.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/index.tsx @@ -21,7 +21,7 @@ import React, { useState, useEffect, useCallback, useMemo } from 'react'; import { cloneDeep, uniq, get } from 'lodash'; import { EuiSpacer } from '@elastic/eui'; -import { IAggConfig } from '../../../legacy_imports'; +import { IAggConfig } from 'src/plugins/data/public'; import { BasicVislibParams, ValueAxis, SeriesParam, Axis } from '../../../types'; import { ValidationVisOptionsProps } from '../../common'; import { SeriesPanel } from './series_panel'; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/gauge.ts b/src/legacy/core_plugins/vis_type_vislib/public/gauge.ts index c78925d5316b0..4610bd37db5f1 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/gauge.ts +++ b/src/legacy/core_plugins/vis_type_vislib/public/gauge.ts @@ -19,8 +19,8 @@ import { i18n } from '@kbn/i18n'; -import { RangeValues } from '../../vis_default_editor/public'; -import { Schemas, AggGroupNames } from './legacy_imports'; +import { RangeValues, Schemas } from '../../vis_default_editor/public'; +import { AggGroupNames } from '../../../../plugins/data/public'; import { GaugeOptions } from './components/options'; import { getGaugeCollections, Alignments, ColorModes, GaugeTypes } from './utils/collections'; import { createVislibVisController } from './vis_controller'; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/goal.ts b/src/legacy/core_plugins/vis_type_vislib/public/goal.ts index d2fdb9543d827..c918128d01f11 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/goal.ts +++ b/src/legacy/core_plugins/vis_type_vislib/public/goal.ts @@ -19,12 +19,13 @@ import { i18n } from '@kbn/i18n'; -import { Schemas, AggGroupNames } from './legacy_imports'; import { GaugeOptions } from './components/options'; import { getGaugeCollections, GaugeTypes, ColorModes } from './utils/collections'; import { createVislibVisController } from './vis_controller'; import { VisTypeVislibDependencies } from './plugin'; import { ColorSchemas } from '../../../../plugins/charts/public'; +import { AggGroupNames } from '../../../../plugins/data/public'; +import { Schemas } from '../../vis_default_editor/public'; export const createGoalVisTypeDefinition = (deps: VisTypeVislibDependencies) => ({ name: 'goal', diff --git a/src/legacy/core_plugins/vis_type_vislib/public/heatmap.ts b/src/legacy/core_plugins/vis_type_vislib/public/heatmap.ts index c8ce335f09e78..39a583f3c9641 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/heatmap.ts +++ b/src/legacy/core_plugins/vis_type_vislib/public/heatmap.ts @@ -19,8 +19,8 @@ import { i18n } from '@kbn/i18n'; -import { RangeValues } from '../../vis_default_editor/public'; -import { Schemas, AggGroupNames } from './legacy_imports'; +import { RangeValues, Schemas } from '../../vis_default_editor/public'; +import { AggGroupNames } from '../../../../plugins/data/public'; import { AxisTypes, getHeatmapCollections, Positions, ScaleTypes } from './utils/collections'; import { HeatmapOptions } from './components/options'; import { createVislibVisController } from './vis_controller'; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/histogram.ts b/src/legacy/core_plugins/vis_type_vislib/public/histogram.ts index 7b9b008481c40..15ef369e5150e 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/histogram.ts +++ b/src/legacy/core_plugins/vis_type_vislib/public/histogram.ts @@ -23,8 +23,8 @@ import { palettes } from '@elastic/eui/lib/services'; // @ts-ignore import { euiPaletteColorBlind } from '@elastic/eui/lib/services'; -import { Schemas, AggGroupNames } from './legacy_imports'; - +import { AggGroupNames } from '../../../../plugins/data/public'; +import { Schemas } from '../../vis_default_editor/public'; import { Positions, ChartTypes, diff --git a/src/legacy/core_plugins/vis_type_vislib/public/horizontal_bar.ts b/src/legacy/core_plugins/vis_type_vislib/public/horizontal_bar.ts index eca26b4f55f60..8b5811628855c 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/horizontal_bar.ts +++ b/src/legacy/core_plugins/vis_type_vislib/public/horizontal_bar.ts @@ -23,8 +23,8 @@ import { palettes } from '@elastic/eui/lib/services'; // @ts-ignore import { euiPaletteColorBlind } from '@elastic/eui/lib/services'; -import { Schemas, AggGroupNames } from './legacy_imports'; - +import { AggGroupNames } from '../../../../plugins/data/public'; +import { Schemas } from '../../vis_default_editor/public'; import { Positions, ChartTypes, 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 343fda44340d1..2b177bff98d56 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,7 +19,6 @@ import { npStart } from 'ui/new_platform'; export const { createFiltersFromEvent } = npStart.plugins.data.actions; -export { AggType, AggGroupNames, IAggConfig, IAggType, Schemas } from 'ui/agg_types'; export { getFormat } from 'ui/visualize/loader/pipeline_helpers/utilities'; import { search } from '../../../../plugins/data/public'; export const { tabifyAggResponse, tabifyGetColumns } = search; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/line.ts b/src/legacy/core_plugins/vis_type_vislib/public/line.ts index 7aaad52ed8841..ac4cda869fe29 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/line.ts +++ b/src/legacy/core_plugins/vis_type_vislib/public/line.ts @@ -23,7 +23,8 @@ import { palettes } from '@elastic/eui/lib/services'; // @ts-ignore import { euiPaletteColorBlind } from '@elastic/eui/lib/services'; -import { Schemas, AggGroupNames } from './legacy_imports'; +import { AggGroupNames } from '../../../../plugins/data/public'; +import { Schemas } from '../../vis_default_editor/public'; import { Positions, ChartTypes, diff --git a/src/legacy/core_plugins/vis_type_vislib/public/pie.ts b/src/legacy/core_plugins/vis_type_vislib/public/pie.ts index b56dba659ffc8..0f1bd93f5b5bd 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/pie.ts +++ b/src/legacy/core_plugins/vis_type_vislib/public/pie.ts @@ -19,7 +19,8 @@ import { i18n } from '@kbn/i18n'; -import { Schemas, AggGroupNames } from './legacy_imports'; +import { AggGroupNames } from '../../../../plugins/data/public'; +import { Schemas } from '../../vis_default_editor/public'; import { PieOptions } from './components/options'; import { getPositions, Positions } from './utils/collections'; import { createVislibVisController } from './vis_controller'; diff --git a/src/legacy/ui/public/agg_types/index.ts b/src/legacy/ui/public/agg_types/index.ts deleted file mode 100644 index 75c2cd4317872..0000000000000 --- a/src/legacy/ui/public/agg_types/index.ts +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -/** - * Nothing to see here! - * - * Agg Types have moved to the new platform, and are being - * re-exported from ui/agg_types for backwards compatibility. - */ - -import { npStart } from 'ui/new_platform'; - -// runtime contracts -const { types } = npStart.plugins.data.search.aggs; -export const aggTypes = types.getAll(); -export const { createAggConfigs } = npStart.plugins.data.search.aggs; -export const { - AggConfig, - AggType, - aggTypeFieldFilters, - FieldParamType, - MetricAggType, - parentPipelineAggHelper, - siblingPipelineAggHelper, -} = npStart.plugins.data.search.__LEGACY; - -// types -export { - AggGroupNames, - AggParam, - AggParamOption, - AggParamType, - AggTypeFieldFilters, - AggTypeFilters, - BUCKET_TYPES, - DateRangeKey, - IAggConfig, - IAggConfigs, - IAggGroupNames, - IAggType, - IFieldParamType, - IMetricAggType, - IpRangeKey, - METRIC_TYPES, - OptionedParamEditorProps, - OptionedParamType, - OptionedValueProp, -} from '../../../../plugins/data/public'; - -// static code -import { search } from '../../../../plugins/data/public'; -export const { - aggGroupNamesMap, - aggTypeFilters, - CidrMask, - convertDateRangeToString, - convertIPRangeToString, - intervalOptions, - isDateHistogramBucketAggConfig, - isStringType, - isType, - isValidInterval, - parentPipelineType, - propFilter, - siblingPipelineType, - termsAggFilter, -} = search.aggs; - -export { ISchemas, Schemas, Schema } from '../../../core_plugins/vis_default_editor/public/schemas'; diff --git a/src/plugins/data/common/field_formats/utils/serialize.ts b/src/plugins/data/common/field_formats/utils/serialize.ts index 9931f55c30a9e..1092c90d19451 100644 --- a/src/plugins/data/common/field_formats/utils/serialize.ts +++ b/src/plugins/data/common/field_formats/utils/serialize.ts @@ -17,7 +17,7 @@ * under the License. */ -import { IAggConfig } from '../../../../../legacy/ui/public/agg_types'; +import { IAggConfig } from 'src/plugins/data/public'; import { SerializedFieldFormat } from '../../../../expressions/common/types'; export const serializeFieldFormat = (agg: IAggConfig): SerializedFieldFormat => { diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 07d8d302bc18c..45ac5a3e12531 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -1322,7 +1322,7 @@ export interface QueryState { // Warning: (ae-missing-release-tag) "QueryStringInput" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const QueryStringInput: React.FC>; +export const QueryStringInput: React.FC>; // @public (undocumented) export type QuerySuggestion = QuerySuggestionBasic | QuerySuggestionField; @@ -1531,8 +1531,8 @@ export const search: { // Warning: (ae-missing-release-tag) "SearchBar" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const SearchBar: React.ComponentClass, "query" | "isLoading" | "indexPatterns" | "filters" | "onQueryChange" | "customSubmitButton" | "screenTitle" | "dataTestSubj" | "showQueryBar" | "showQueryInput" | "showFilterBar" | "showDatePicker" | "showAutoRefreshOnly" | "isRefreshPaused" | "refreshInterval" | "dateRangeFrom" | "dateRangeTo" | "showSaveQuery" | "savedQuery" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated" | "onClearSavedQuery" | "onRefresh" | "timeHistory" | "onFiltersUpdated" | "onRefreshChange">, any> & { - WrappedComponent: React.ComponentType & ReactIntl.InjectedIntlProps>; +export const SearchBar: React.ComponentClass, "query" | "isLoading" | "indexPatterns" | "filters" | "refreshInterval" | "screenTitle" | "dataTestSubj" | "customSubmitButton" | "showQueryBar" | "showQueryInput" | "showFilterBar" | "showDatePicker" | "showAutoRefreshOnly" | "isRefreshPaused" | "dateRangeFrom" | "dateRangeTo" | "showSaveQuery" | "savedQuery" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated" | "onClearSavedQuery" | "onRefresh" | "timeHistory" | "onFiltersUpdated" | "onRefreshChange">, any> & { + WrappedComponent: React.ComponentType & ReactIntl.InjectedIntlProps>; }; // Warning: (ae-forgotten-export) The symbol "SearchBarOwnProps" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/kibana_legacy/public/index.ts b/src/plugins/kibana_legacy/public/index.ts index 18f01854de259..75e81b0505747 100644 --- a/src/plugins/kibana_legacy/public/index.ts +++ b/src/plugins/kibana_legacy/public/index.ts @@ -27,6 +27,7 @@ export * from './plugin'; export { kbnBaseUrl } from '../common/kbn_base_url'; export { initAngularBootstrap } from './angular_bootstrap'; +export { PaginateDirectiveProvider, PaginateControlsDirectiveProvider } from './paginate/paginate'; export * from './angular'; export * from './notify'; export * from './utils'; diff --git a/src/plugins/kibana_legacy/public/paginate/paginate.d.ts b/src/plugins/kibana_legacy/public/paginate/paginate.d.ts new file mode 100644 index 0000000000000..a40b869b2ccbb --- /dev/null +++ b/src/plugins/kibana_legacy/public/paginate/paginate.d.ts @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export function PaginateDirectiveProvider($parse: any, $compile: any): any; +export function PaginateControlsDirectiveProvider(): any; diff --git a/src/legacy/ui/public/directives/paginate.js b/src/plugins/kibana_legacy/public/paginate/paginate.js similarity index 93% rename from src/legacy/ui/public/directives/paginate.js rename to src/plugins/kibana_legacy/public/paginate/paginate.js index 802aaaf453751..f7e623cdabd86 100644 --- a/src/legacy/ui/public/directives/paginate.js +++ b/src/plugins/kibana_legacy/public/paginate/paginate.js @@ -19,8 +19,7 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; -import { uiModules } from '../modules'; -import paginateControlsTemplate from './partials/paginate_controls.html'; +import paginateControlsTemplate from './paginate_controls.html'; export function PaginateDirectiveProvider($parse, $compile) { return { @@ -61,12 +60,9 @@ export function PaginateDirectiveProvider($parse, $compile) { controller: function($scope, $document) { const self = this; const ALL = 0; - const allSizeTitle = i18n.translate( - 'common.ui.directives.paginate.size.allDropDownOptionLabel', - { - defaultMessage: 'All', - } - ); + const allSizeTitle = i18n.translate('kibana_legacy.paginate.size.allDropDownOptionLabel', { + defaultMessage: 'All', + }); self.sizeOptions = [ { title: '10', value: 10 }, @@ -229,8 +225,3 @@ export function PaginateControlsDirectiveProvider() { template: paginateControlsTemplate, }; } - -uiModules - .get('kibana') - .directive('paginate', PaginateDirectiveProvider) - .directive('paginateControls', PaginateControlsDirectiveProvider); diff --git a/src/legacy/ui/public/directives/partials/paginate_controls.html b/src/plugins/kibana_legacy/public/paginate/paginate_controls.html similarity index 96% rename from src/legacy/ui/public/directives/partials/paginate_controls.html rename to src/plugins/kibana_legacy/public/paginate/paginate_controls.html index c40021507c233..a553bc2231720 100644 --- a/src/legacy/ui/public/directives/partials/paginate_controls.html +++ b/src/plugins/kibana_legacy/public/paginate/paginate_controls.html @@ -3,7 +3,7 @@ ng-if="linkToTop" ng-click="paginate.goToTop()" data-test-subj="paginateControlsLinkToTop" - i18n-id="common.ui.paginateControls.scrollTopButtonLabel" + i18n-id="kibana_legacy.paginate.controls.scrollTopButtonLabel" i18n-default-message="Scroll to top" > @@ -86,7 +86,7 @@