diff --git a/src/legacy/core_plugins/kibana/public/dashboard/__tests__/get_saved_dashboard_mock.ts b/src/legacy/core_plugins/kibana/public/dashboard/__tests__/get_saved_dashboard_mock.ts index 01468eadffb8..bf7135098ea7 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/__tests__/get_saved_dashboard_mock.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/__tests__/get_saved_dashboard_mock.ts @@ -41,5 +41,5 @@ export function getSavedDashboardMock( getQuery: () => ({ query: '', language: 'kuery' }), getFilters: () => [], ...config, - }; + } as SavedObjectDashboard; } diff --git a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app_controller.tsx b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app_controller.tsx index d8496ebb5cdb..24b64a88998f 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app_controller.tsx +++ b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app_controller.tsx @@ -35,7 +35,7 @@ import { State, AppStateClass as TAppStateClass, KbnUrl, - SaveOptions, + SavedObjectSaveOpts, unhashUrl, } from './legacy_imports'; import { FilterStateManager, IndexPattern } from '../../../data/public'; @@ -608,7 +608,7 @@ export class DashboardAppController { * @return {Promise} * @resolved {String} - The id of the doc */ - function save(saveOptions: SaveOptions): Promise { + function save(saveOptions: SavedObjectSaveOpts): Promise { return saveDashboard(angular.toJson, timefilter, dashboardStateManager, saveOptions) .then(function(id) { if (id) { diff --git a/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts b/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts index af0a833399a5..adae063a1470 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts @@ -30,7 +30,7 @@ export const legacyChrome = chrome; export { State } from 'ui/state_management/state'; export { AppState } from 'ui/state_management/app_state'; export { AppStateClass } from 'ui/state_management/app_state'; -export { SaveOptions } from 'ui/saved_objects/saved_object'; +export { SavedObjectSaveOpts } from 'ui/saved_objects/types'; export { npSetup, npStart } from 'ui/new_platform'; export { SavedObjectRegistryProvider } from 'ui/saved_objects'; export { IPrivate } from 'ui/private'; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/lib/save_dashboard.ts b/src/legacy/core_plugins/kibana/public/dashboard/lib/save_dashboard.ts index e0d82373d3ad..e322677433ce 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/lib/save_dashboard.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/lib/save_dashboard.ts @@ -18,7 +18,7 @@ */ import { TimefilterContract } from 'src/plugins/data/public'; -import { SaveOptions } from '../legacy_imports'; +import { SavedObjectSaveOpts } from '../legacy_imports'; import { updateSavedDashboard } from './update_saved_dashboard'; import { DashboardStateManager } from '../dashboard_state_manager'; @@ -34,7 +34,7 @@ export function saveDashboard( toJson: (obj: any) => string, timeFilter: TimefilterContract, dashboardStateManager: DashboardStateManager, - saveOptions: SaveOptions + saveOptions: SavedObjectSaveOpts ): Promise { dashboardStateManager.saveState(); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard.d.ts b/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard.d.ts index 4c417ed2954d..20544fa97fdb 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard.d.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard.d.ts @@ -17,7 +17,7 @@ * under the License. */ -import { SavedObject } from 'ui/saved_objects/saved_object'; +import { SavedObject } from 'ui/saved_objects/types'; import { SearchSourceContract } from '../../../../../ui/public/courier'; import { esFilters, Query, RefreshInterval } from '../../../../../../plugins/data/public'; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboards.js b/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboards.js index 9b7d036590f1..6b561c18a5d4 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboards.js +++ b/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboards.js @@ -22,6 +22,7 @@ import './saved_dashboard'; import { uiModules } from 'ui/modules'; import { SavedObjectLoader, SavedObjectsClientProvider } from 'ui/saved_objects'; import { savedObjectManagementRegistry } from '../../management/saved_object_registry'; +import { npStart } from '../../../../../ui/public/new_platform'; const module = uiModules.get('app/dashboard'); @@ -35,7 +36,7 @@ savedObjectManagementRegistry.register({ }); // This is the only thing that gets injected into controllers -module.service('savedDashboards', function (Private, SavedDashboard, kbnUrl, chrome) { +module.service('savedDashboards', function (Private, SavedDashboard) { const savedObjectClient = Private(SavedObjectsClientProvider); - return new SavedObjectLoader(SavedDashboard, kbnUrl, chrome, savedObjectClient); + return new SavedObjectLoader(SavedDashboard, savedObjectClient, npStart.core.chrome); }); diff --git a/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/discover_field.js b/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/discover_field.js index 92df04c536e4..5c6ba8645558 100644 --- a/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/discover_field.js +++ b/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/discover_field.js @@ -32,7 +32,7 @@ describe('discoverField', function () { let $scope; let indexPattern; let $elem; - beforeEach(() => pluginInstance.initializeServices(true)); + beforeEach(() => pluginInstance.initializeServices()); beforeEach(() => pluginInstance.initializeInnerAngular()); beforeEach(ngMock.module('app/discover')); beforeEach(ngMock.inject(function (Private, $rootScope, $compile) { 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 34c6483349af..a707402ff92b 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 @@ -70,7 +70,7 @@ describe('discover field chooser directives', function () { on-remove-field="removeField" > `); - beforeEach(() => pluginInstance.initializeServices(true)); + beforeEach(() => pluginInstance.initializeServices()); beforeEach(() => pluginInstance.initializeInnerAngular()); beforeEach(ngMock.module('app/discover', ($provide) => { diff --git a/src/legacy/core_plugins/kibana/public/discover/angular/context/query_parameters/__tests__/action_add_filter.js b/src/legacy/core_plugins/kibana/public/discover/angular/context/query_parameters/__tests__/action_add_filter.js index 645ca32924ed..aa3c260eed52 100644 --- a/src/legacy/core_plugins/kibana/public/discover/angular/context/query_parameters/__tests__/action_add_filter.js +++ b/src/legacy/core_plugins/kibana/public/discover/angular/context/query_parameters/__tests__/action_add_filter.js @@ -27,7 +27,7 @@ import { npStart } from 'ui/new_platform'; describe('context app', function () { beforeEach(() => pluginInstance.initializeInnerAngular()); - beforeEach(() => pluginInstance.initializeServices(true)); + beforeEach(() => pluginInstance.initializeServices()); beforeEach(ngMock.module('app/discover')); beforeEach(ngMock.module(function createServiceStubs($provide) { $provide.value('indexPatterns', createIndexPatternsStub()); diff --git a/src/legacy/core_plugins/kibana/public/discover/angular/context/query_parameters/__tests__/action_set_predecessor_count.js b/src/legacy/core_plugins/kibana/public/discover/angular/context/query_parameters/__tests__/action_set_predecessor_count.js index a8bef6fe75c7..e1821eada79e 100644 --- a/src/legacy/core_plugins/kibana/public/discover/angular/context/query_parameters/__tests__/action_set_predecessor_count.js +++ b/src/legacy/core_plugins/kibana/public/discover/angular/context/query_parameters/__tests__/action_set_predecessor_count.js @@ -26,7 +26,7 @@ import { getQueryParameterActions } from '../actions'; describe('context app', function () { beforeEach(() => pluginInstance.initializeInnerAngular()); - beforeEach(() => pluginInstance.initializeServices(true)); + beforeEach(() => pluginInstance.initializeServices()); beforeEach(ngMock.module('app/discover')); describe('action setPredecessorCount', function () { diff --git a/src/legacy/core_plugins/kibana/public/discover/angular/context/query_parameters/__tests__/action_set_query_parameters.js b/src/legacy/core_plugins/kibana/public/discover/angular/context/query_parameters/__tests__/action_set_query_parameters.js index a43a8a11a7bf..348ea2c55790 100644 --- a/src/legacy/core_plugins/kibana/public/discover/angular/context/query_parameters/__tests__/action_set_query_parameters.js +++ b/src/legacy/core_plugins/kibana/public/discover/angular/context/query_parameters/__tests__/action_set_query_parameters.js @@ -27,7 +27,7 @@ import { getQueryParameterActions } from '../actions'; describe('context app', function () { beforeEach(() => pluginInstance.initializeInnerAngular()); - beforeEach(() => pluginInstance.initializeServices(true)); + beforeEach(() => pluginInstance.initializeServices()); beforeEach(ngMock.module('app/discover')); describe('action setQueryParameters', function () { diff --git a/src/legacy/core_plugins/kibana/public/discover/angular/context/query_parameters/__tests__/action_set_successor_count.js b/src/legacy/core_plugins/kibana/public/discover/angular/context/query_parameters/__tests__/action_set_successor_count.js index 4bbd462aaa4b..c03158722347 100644 --- a/src/legacy/core_plugins/kibana/public/discover/angular/context/query_parameters/__tests__/action_set_successor_count.js +++ b/src/legacy/core_plugins/kibana/public/discover/angular/context/query_parameters/__tests__/action_set_successor_count.js @@ -27,7 +27,7 @@ import { getQueryParameterActions } from '../actions'; describe('context app', function () { beforeEach(() => pluginInstance.initializeInnerAngular()); - beforeEach(() => pluginInstance.initializeServices(true)); + beforeEach(() => pluginInstance.initializeServices()); beforeEach(ngMock.module('app/discover')); describe('action setSuccessorCount', function () { diff --git a/src/legacy/core_plugins/kibana/public/discover/angular/doc_table/__tests__/lib/rows_headers.js b/src/legacy/core_plugins/kibana/public/discover/angular/doc_table/__tests__/lib/rows_headers.js index 666261610fa7..c8e8e216d0de 100644 --- a/src/legacy/core_plugins/kibana/public/discover/angular/doc_table/__tests__/lib/rows_headers.js +++ b/src/legacy/core_plugins/kibana/public/discover/angular/doc_table/__tests__/lib/rows_headers.js @@ -37,7 +37,7 @@ describe('Doc Table', function () { let fakeRowVals; let stubFieldFormatConverter; - beforeEach(() => pluginInstance.initializeServices(true)); + beforeEach(() => pluginInstance.initializeServices()); beforeEach(() => pluginInstance.initializeInnerAngular()); beforeEach(ngMock.module('app/discover')); beforeEach( diff --git a/src/legacy/core_plugins/kibana/public/discover/helpers/build_services.ts b/src/legacy/core_plugins/kibana/public/discover/helpers/build_services.ts index b72bd27a31cf..14ea4e99b0de 100644 --- a/src/legacy/core_plugins/kibana/public/discover/helpers/build_services.ts +++ b/src/legacy/core_plugins/kibana/public/discover/helpers/build_services.ts @@ -25,14 +25,12 @@ import { IUiSettingsClient, } from 'kibana/public'; import * as docViewsRegistry from 'ui/registry/doc_views'; -import chromeLegacy from 'ui/chrome'; -import { IPrivate } from 'ui/private'; import { FilterManager, TimefilterContract, IndexPatternsContract } from 'src/plugins/data/public'; // @ts-ignore import { createSavedSearchesService } from '../saved_searches/saved_searches'; // @ts-ignore -import { createSavedSearchFactory } from '../saved_searches/_saved_search'; import { DiscoverStartPlugins } from '../plugin'; +import { DataStart } from '../../../../data/public'; import { EuiUtilsStart } from '../../../../../../plugins/eui_utils/public'; import { SavedSearch } from '../types'; import { SharePluginStart } from '../../../../../../plugins/share/public'; @@ -42,6 +40,7 @@ export interface DiscoverServices { capabilities: Capabilities; chrome: ChromeStart; core: CoreStart; + data: DataStart; docLinks: DocLinksStart; docViewsRegistry: docViewsRegistry.DocViewsRegistry; eui_utils: EuiUtilsStart; @@ -52,35 +51,19 @@ export interface DiscoverServices { share: SharePluginStart; timefilter: TimefilterContract; toastNotifications: ToastsStart; - // legacy getSavedSearchById: (id: string) => Promise; getSavedSearchUrlById: (id: string) => Promise; uiSettings: IUiSettingsClient; } - -export async function buildGlobalAngularServices() { - const injector = await chromeLegacy.dangerouslyGetActiveInjector(); - const Private = injector.get('Private'); - const kbnUrl = injector.get('kbnUrl'); - const SavedSearchFactory = createSavedSearchFactory(Private); - const service = createSavedSearchesService(Private, SavedSearchFactory, kbnUrl, chromeLegacy); - return { - getSavedSearchById: async (id: string) => service.get(id), - getSavedSearchUrlById: async (id: string) => service.urlFor(id), +export async function buildServices(core: CoreStart, plugins: DiscoverStartPlugins) { + const services = { + savedObjectsClient: core.savedObjects.client, + indexPatterns: plugins.data.indexPatterns, + chrome: core.chrome, + overlays: core.overlays, }; -} - -export async function buildServices(core: CoreStart, plugins: DiscoverStartPlugins, test: false) { - const globalAngularServices = !test - ? await buildGlobalAngularServices() - : { - getSavedSearchById: async (id: string) => void id, - getSavedSearchUrlById: async (id: string) => void id, - State: null, - }; - + const savedObjectService = createSavedSearchesService(services); return { - ...globalAngularServices, addBasePath: core.http.basePath.prepend, capabilities: core.application.capabilities, chrome: core.chrome, @@ -90,6 +73,8 @@ export async function buildServices(core: CoreStart, plugins: DiscoverStartPlugi docViewsRegistry, eui_utils: plugins.eui_utils, filterManager: plugins.data.query.filterManager, + getSavedSearchById: async (id: string) => savedObjectService.get(id), + getSavedSearchUrlById: async (id: string) => savedObjectService.urlFor(id), indexPatterns: plugins.data.indexPatterns, inspector: plugins.inspector, // @ts-ignore diff --git a/src/legacy/core_plugins/kibana/public/discover/plugin.ts b/src/legacy/core_plugins/kibana/public/discover/plugin.ts index 6679e8a3b882..a5a1ead93188 100644 --- a/src/legacy/core_plugins/kibana/public/discover/plugin.ts +++ b/src/legacy/core_plugins/kibana/public/discover/plugin.ts @@ -106,11 +106,11 @@ export class DiscoverPlugin implements Plugin { this.innerAngularInitialized = true; }; - this.initializeServices = async (test = false) => { + this.initializeServices = async () => { if (this.servicesInitialized) { return; } - const services = await buildServices(core, plugins, test); + const services = await buildServices(core, plugins); setServices(services); this.servicesInitialized = true; }; diff --git a/src/legacy/core_plugins/kibana/public/discover/saved_searches/_saved_search.js b/src/legacy/core_plugins/kibana/public/discover/saved_searches/_saved_search.js deleted file mode 100644 index db2b2b5b22af..000000000000 --- a/src/legacy/core_plugins/kibana/public/discover/saved_searches/_saved_search.js +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { createLegacyClass } from 'ui/utils/legacy_class'; -import { SavedObjectProvider } from 'ui/saved_objects/saved_object'; - -import { uiModules } from 'ui/modules'; - -const module = uiModules.get('discover/saved_searches', []); - -export function createSavedSearchFactory(Private) { - const SavedObject = Private(SavedObjectProvider); - createLegacyClass(SavedSearch).inherits(SavedObject); - function SavedSearch(id) { - SavedObject.call(this, { - type: SavedSearch.type, - mapping: SavedSearch.mapping, - searchSource: SavedSearch.searchSource, - id: id, - defaults: { - title: '', - description: '', - columns: [], - hits: 0, - sort: [], - version: 1, - }, - }); - - this.showInRecentlyAccessed = true; - } - - SavedSearch.type = 'search'; - - SavedSearch.mapping = { - title: 'text', - description: 'text', - hits: 'integer', - columns: 'keyword', - sort: 'keyword', - version: 'integer', - }; - - // Order these fields to the top, the rest are alphabetical - SavedSearch.fieldOrder = ['title', 'description']; - - SavedSearch.searchSource = true; - - SavedSearch.prototype.getFullPath = function () { - return `/app/kibana#/discover/${this.id}`; - }; - - return SavedSearch; -} - -module.factory('SavedSearch', createSavedSearchFactory); diff --git a/src/legacy/core_plugins/kibana/public/discover/saved_searches/_saved_search.ts b/src/legacy/core_plugins/kibana/public/discover/saved_searches/_saved_search.ts new file mode 100644 index 000000000000..113d13287bd1 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/discover/saved_searches/_saved_search.ts @@ -0,0 +1,71 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { SavedObjectKibanaServices } from 'ui/saved_objects/types'; +import { createSavedObjectClass } from 'ui/saved_objects/saved_object'; + +export function createSavedSearchClass(services: SavedObjectKibanaServices) { + const SavedObjectClass = createSavedObjectClass(services); + + class SavedSearch extends SavedObjectClass { + public static type: string = 'search'; + public static mapping = { + title: 'text', + description: 'text', + hits: 'integer', + columns: 'keyword', + sort: 'keyword', + version: 'integer', + }; + // Order these fields to the top, the rest are alphabetical + public static fieldOrder = ['title', 'description']; + public static searchSource = true; + + public id: string; + public showInRecentlyAccessed: boolean; + + constructor(id: string) { + super({ + id, + type: 'search', + mapping: { + title: 'text', + description: 'text', + hits: 'integer', + columns: 'keyword', + sort: 'keyword', + version: 'integer', + }, + searchSource: true, + defaults: { + title: '', + description: '', + columns: [], + hits: 0, + sort: [], + version: 1, + }, + }); + this.showInRecentlyAccessed = true; + this.id = id; + this.getFullPath = () => `/app/kibana#/discover/${String(id)}`; + } + } + + return SavedSearch; +} diff --git a/src/legacy/core_plugins/kibana/public/discover/saved_searches/index.js b/src/legacy/core_plugins/kibana/public/discover/saved_searches/index.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/discover/saved_searches/index.js rename to src/legacy/core_plugins/kibana/public/discover/saved_searches/index.ts diff --git a/src/legacy/core_plugins/kibana/public/discover/saved_searches/saved_searches.js b/src/legacy/core_plugins/kibana/public/discover/saved_searches/saved_searches.ts similarity index 60% rename from src/legacy/core_plugins/kibana/public/discover/saved_searches/saved_searches.js rename to src/legacy/core_plugins/kibana/public/discover/saved_searches/saved_searches.ts index 7ebcba903cc7..46fdd3a7baed 100644 --- a/src/legacy/core_plugins/kibana/public/discover/saved_searches/saved_searches.js +++ b/src/legacy/core_plugins/kibana/public/discover/saved_searches/saved_searches.ts @@ -16,12 +16,14 @@ * specific language governing permissions and limitations * under the License. */ - -import './_saved_search'; +import { npStart } from 'ui/new_platform'; +// @ts-ignore import { uiModules } from 'ui/modules'; -import { SavedObjectLoader, SavedObjectsClientProvider } from 'ui/saved_objects'; +import { SavedObjectLoader } from 'ui/saved_objects'; +import { SavedObjectKibanaServices } from 'ui/saved_objects/types'; +// @ts-ignore import { savedObjectManagementRegistry } from '../../management/saved_object_registry'; - +import { createSavedSearchClass } from './_saved_search'; // Register this service with the saved object registry so it can be // edited by the object editor. @@ -30,9 +32,13 @@ savedObjectManagementRegistry.register({ title: 'searches', }); -export function createSavedSearchesService(Private, SavedSearch, kbnUrl, chrome) { - const savedObjectClient = Private(SavedObjectsClientProvider); - const savedSearchLoader = new SavedObjectLoader(SavedSearch, kbnUrl, chrome, savedObjectClient); +export function createSavedSearchesService(services: SavedObjectKibanaServices) { + const SavedSearchClass = createSavedSearchClass(services); + const savedSearchLoader = new SavedObjectLoader( + SavedSearchClass, + services.savedObjectsClient, + services.chrome + ); // Customize loader properties since adding an 's' on type doesn't work for type 'search' . savedSearchLoader.loaderProperties = { name: 'searches', @@ -40,11 +46,18 @@ export function createSavedSearchesService(Private, SavedSearch, kbnUrl, chrome) nouns: 'saved searches', }; - savedSearchLoader.urlFor = (id) => { - return kbnUrl.eval('#/discover/{{id}}', { id: id }); - }; + savedSearchLoader.urlFor = (id: string) => `#/discover/${encodeURIComponent(id)}`; return savedSearchLoader; } +// this is needed for saved object management const module = uiModules.get('discover/saved_searches'); -module.service('savedSearches', createSavedSearchesService); +module.service('savedSearches', () => { + const services = { + savedObjectsClient: npStart.core.savedObjects.client, + indexPatterns: npStart.plugins.data.indexPatterns, + chrome: npStart.core.chrome, + overlays: npStart.core.overlays, + }; + return createSavedSearchesService(services); +}); diff --git a/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable.ts b/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable.ts index 7ab60f8867c3..8e414984a0c0 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable.ts @@ -22,7 +22,7 @@ import { PersistedState } from 'ui/persisted_state'; import { Subscription } from 'rxjs'; import * as Rx from 'rxjs'; import { buildPipeline } from 'ui/visualize/loader/pipeline_helpers'; -import { SavedObject } from 'ui/saved_objects/saved_object'; +import { SavedObject } from 'ui/saved_objects/types'; import { Vis } from 'ui/vis'; import { queryGeohashBounds } from 'ui/visualize/loader/utils'; import { getTableAggs } from 'ui/visualize/loader/pipeline_helpers/utilities'; diff --git a/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/_saved_vis.js b/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/_saved_vis.js index ae4b4d1c779d..a30973ab6a46 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/_saved_vis.js +++ b/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/_saved_vis.js @@ -38,7 +38,7 @@ import { uiModules .get('app/visualize') - .factory('SavedVis', function (Promise, savedSearches, Private) { + .factory('SavedVis', function (savedSearches, Private) { const SavedObject = Private(SavedObjectProvider); createLegacyClass(SavedVis).inherits(SavedObject); function SavedVis(opts) { @@ -95,18 +95,15 @@ uiModules return `/app/kibana#${VisualizeConstants.EDIT_PATH}/${this.id}`; }; - SavedVis.prototype._afterEsResp = function () { + SavedVis.prototype._afterEsResp = async function () { const self = this; - return self._getLinkedSavedSearch() - .then(function () { - self.searchSource.setField('size', 0); - - return self.vis ? self._updateVis() : self._createVis(); - }); + await self._getLinkedSavedSearch(); + self.searchSource.setField('size', 0); + return self.vis ? self._updateVis() : self._createVis(); }; - SavedVis.prototype._getLinkedSavedSearch = Promise.method(function () { + SavedVis.prototype._getLinkedSavedSearch = async function () { const self = this; const linkedSearch = !!self.savedSearchId; const current = self.savedSearch; @@ -122,13 +119,10 @@ uiModules } if (linkedSearch) { - return savedSearches.get(self.savedSearchId) - .then(function (savedSearch) { - self.savedSearch = savedSearch; - self.searchSource.setParent(self.savedSearch.searchSource); - }); + self.savedSearch = await savedSearches.get(self.savedSearchId); + self.searchSource.setParent(self.savedSearch.searchSource); } - }); + }; SavedVis.prototype._createVis = function () { const self = this; diff --git a/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/saved_visualizations.js b/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/saved_visualizations.js index c391eb872e29..dd12b7e38798 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/saved_visualizations.js +++ b/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/saved_visualizations.js @@ -24,6 +24,7 @@ import { savedObjectManagementRegistry } from '../../management/saved_object_reg import { start as visualizations } from '../../../../visualizations/public/np_ready/public/legacy'; import { createVisualizeEditUrl } from '../visualize_constants'; import { findListItems } from './find_list_items'; +import { npStart } from '../../../../../ui/public/new_platform'; const app = uiModules.get('app/visualize'); @@ -34,14 +35,13 @@ savedObjectManagementRegistry.register({ title: 'visualizations', }); -app.service('savedVisualizations', function (SavedVis, Private, kbnUrl, chrome) { +app.service('savedVisualizations', function (SavedVis, Private) { const visTypes = visualizations.types; const savedObjectClient = Private(SavedObjectsClientProvider); const saveVisualizationLoader = new SavedObjectLoader( SavedVis, - kbnUrl, - chrome, - savedObjectClient + savedObjectClient, + npStart.core.chrome ); saveVisualizationLoader.mapHitSource = function (source, id) { @@ -73,7 +73,7 @@ app.service('savedVisualizations', function (SavedVis, Private, kbnUrl, chrome) }; saveVisualizationLoader.urlFor = function (id) { - return kbnUrl.eval('#/visualize/edit/{{id}}', { id: id }); + return `#/visualize/edit/${encodeURIComponent(id)}`; }; // This behaves similarly to find, except it returns visualizations that are diff --git a/src/legacy/core_plugins/timelion/public/services/saved_sheets.js b/src/legacy/core_plugins/timelion/public/services/saved_sheets.js index d303069e74de..86cde9e06240 100644 --- a/src/legacy/core_plugins/timelion/public/services/saved_sheets.js +++ b/src/legacy/core_plugins/timelion/public/services/saved_sheets.js @@ -21,6 +21,7 @@ import { SavedObjectLoader, SavedObjectsClientProvider } from 'ui/saved_objects' import { savedObjectManagementRegistry } from 'plugins/kibana/management/saved_object_registry'; import { uiModules } from 'ui/modules'; import './_saved_sheet.js'; +import { npStart } from '../../../../ui/public/new_platform'; const module = uiModules.get('app/sheet'); @@ -32,9 +33,9 @@ savedObjectManagementRegistry.register({ }); // This is the only thing that gets injected into controllers -module.service('savedSheets', function (Private, SavedSheet, kbnUrl, chrome) { +module.service('savedSheets', function (Private, SavedSheet, kbnUrl) { const savedObjectClient = Private(SavedObjectsClientProvider); - const savedSheetLoader = new SavedObjectLoader(SavedSheet, kbnUrl, chrome, savedObjectClient); + const savedSheetLoader = new SavedObjectLoader(SavedSheet, savedObjectClient, npStart.core.chrome); savedSheetLoader.urlFor = function (id) { return kbnUrl.eval('#/{{id}}', { id: id }); }; diff --git a/src/legacy/ui/public/saved_objects/__tests__/find_object_by_title.js b/src/legacy/ui/public/saved_objects/__tests__/find_object_by_title.js index 766ed44a4c0f..ccd2ba6a6c51 100644 --- a/src/legacy/ui/public/saved_objects/__tests__/find_object_by_title.js +++ b/src/legacy/ui/public/saved_objects/__tests__/find_object_by_title.js @@ -19,7 +19,7 @@ import sinon from 'sinon'; import expect from '@kbn/expect'; -import { findObjectByTitle } from '../find_object_by_title'; +import { findObjectByTitle } from '../helpers/find_object_by_title'; import { SimpleSavedObject } from '../../../../../core/public'; describe('findObjectByTitle', () => { diff --git a/src/legacy/ui/public/saved_objects/__tests__/saved_object.js b/src/legacy/ui/public/saved_objects/__tests__/saved_object.js index 56124a047ba6..55d0f800ae7f 100644 --- a/src/legacy/ui/public/saved_objects/__tests__/saved_object.js +++ b/src/legacy/ui/public/saved_objects/__tests__/saved_object.js @@ -22,7 +22,7 @@ import expect from '@kbn/expect'; import sinon from 'sinon'; import Bluebird from 'bluebird'; -import { SavedObjectProvider } from '../saved_object'; +import { createSavedObjectClass } from '../saved_object'; import StubIndexPattern from 'test_utils/stub_index_pattern'; import { SavedObjectsClientProvider } from '../saved_objects_client_provider'; import { InvalidJSONProperty } from '../../../../../plugins/kibana_utils/public'; @@ -97,9 +97,9 @@ describe('Saved Object', function () { ); beforeEach(ngMock.inject(function (es, Private, $window) { - SavedObject = Private(SavedObjectProvider); - esDataStub = es; savedObjectsClientStub = Private(SavedObjectsClientProvider); + SavedObject = createSavedObjectClass({ savedObjectsClient: savedObjectsClientStub }); + esDataStub = es; window = $window; })); @@ -110,66 +110,6 @@ describe('Saved Object', function () { sinon.stub(esDataStub, 'create').returns(Bluebird.reject(mock409FetchError)); } - describe('when true', function () { - it('requests confirmation and updates on yes response', function () { - stubESResponse(getMockedDocResponse('myId')); - return createInitializedSavedObject({ type: 'dashboard', id: 'myId' }).then(savedObject => { - const createStub = sinon.stub(savedObjectsClientStub, 'create'); - createStub.onFirstCall().returns(Bluebird.reject(mock409FetchError)); - createStub.onSecondCall().returns(Bluebird.resolve({ id: 'myId' })); - - stubConfirmOverwrite(); - - savedObject.lastSavedTitle = 'original title'; - savedObject.title = 'new title'; - return savedObject.save({ confirmOverwrite: true }) - .then(() => { - expect(window.confirm.called).to.be(true); - expect(savedObject.id).to.be('myId'); - expect(savedObject.isSaving).to.be(false); - expect(savedObject.lastSavedTitle).to.be('new title'); - expect(savedObject.title).to.be('new title'); - }); - }); - }); - - it('does not update on no response', function () { - stubESResponse(getMockedDocResponse('HI')); - return createInitializedSavedObject({ type: 'dashboard', id: 'HI' }).then(savedObject => { - window.confirm = sinon.stub().returns(false); - - sinon.stub(savedObjectsClientStub, 'create').returns(Bluebird.reject(mock409FetchError)); - - savedObject.lastSavedTitle = 'original title'; - savedObject.title = 'new title'; - return savedObject.save({ confirmOverwrite: true }) - .then(() => { - expect(savedObject.id).to.be('HI'); - expect(savedObject.isSaving).to.be(false); - expect(savedObject.lastSavedTitle).to.be('original title'); - expect(savedObject.title).to.be('new title'); - }); - }); - }); - - it('handles create failures', function () { - stubESResponse(getMockedDocResponse('myId')); - return createInitializedSavedObject({ type: 'dashboard', id: 'myId' }).then(savedObject => { - stubConfirmOverwrite(); - - sinon.stub(savedObjectsClientStub, 'create').returns(Bluebird.reject(mock409FetchError)); - - return savedObject.save({ confirmOverwrite: true }) - .then(() => { - expect(true).to.be(false); // Force failure, the save should not succeed. - }) - .catch(() => { - expect(window.confirm.called).to.be(true); - }); - }); - }); - }); - it('when false does not request overwrite', function () { const mockDocResponse = getMockedDocResponse('myId'); stubESResponse(mockDocResponse); @@ -691,18 +631,6 @@ describe('Saved Object', function () { }); }); - it('init is called', function () { - const initCallback = sinon.spy(); - const config = { - type: 'dashboard', - init: initCallback - }; - - return createInitializedSavedObject(config).then(() => { - expect(initCallback.called).to.be(true); - }); - }); - describe('searchSource', function () { it('when true, creates index', function () { const indexPatternId = 'testIndexPattern'; diff --git a/src/legacy/ui/public/saved_objects/saved_object.d.ts b/src/legacy/ui/public/saved_objects/constants.ts similarity index 55% rename from src/legacy/ui/public/saved_objects/saved_object.d.ts rename to src/legacy/ui/public/saved_objects/constants.ts index dc6496eacfcb..e1684aa1d19d 100644 --- a/src/legacy/ui/public/saved_objects/saved_object.d.ts +++ b/src/legacy/ui/public/saved_objects/constants.ts @@ -16,15 +16,25 @@ * specific language governing permissions and limitations * under the License. */ +import { i18n } from '@kbn/i18n'; -export interface SaveOptions { - confirmOverwrite: boolean; - isTitleDuplicateConfirmed: boolean; - onTitleDuplicate: () => void; -} - -export interface SavedObject { - save: (saveOptions: SaveOptions) => Promise; - copyOnSave: boolean; - id?: string; -} +/** + * An error message to be used when the user rejects a confirm overwrite. + * @type {string} + */ +export const OVERWRITE_REJECTED = i18n.translate( + 'common.ui.savedObjects.overwriteRejectedDescription', + { + defaultMessage: 'Overwrite confirmation was rejected', + } +); +/** + * An error message to be used when the user rejects a confirm save with duplicate title. + * @type {string} + */ +export const SAVE_DUPLICATE_REJECTED = i18n.translate( + 'common.ui.savedObjects.saveDuplicateRejectedDescription', + { + defaultMessage: 'Save with duplicate title confirmation was rejected', + } +); diff --git a/src/legacy/ui/public/saved_objects/helpers/apply_es_resp.ts b/src/legacy/ui/public/saved_objects/helpers/apply_es_resp.ts new file mode 100644 index 000000000000..77f504d10807 --- /dev/null +++ b/src/legacy/ui/public/saved_objects/helpers/apply_es_resp.ts @@ -0,0 +1,82 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import _ from 'lodash'; +import { EsResponse, SavedObject, SavedObjectConfig } from 'ui/saved_objects/types'; +import { parseSearchSource } from 'ui/saved_objects/helpers/parse_search_source'; +import { expandShorthand, SavedObjectNotFound } from '../../../../../plugins/kibana_utils/public'; +import { IndexPattern } from '../../../../core_plugins/data/public'; + +/** + * A given response of and ElasticSearch containing a plain saved object is applied to the given + * savedObject + */ +export async function applyESResp( + resp: EsResponse, + savedObject: SavedObject, + config: SavedObjectConfig +) { + const mapping = expandShorthand(config.mapping); + const esType = config.type || ''; + savedObject._source = _.cloneDeep(resp._source); + const injectReferences = config.injectReferences; + const hydrateIndexPattern = savedObject.hydrateIndexPattern!; + if (typeof resp.found === 'boolean' && !resp.found) { + throw new SavedObjectNotFound(esType, savedObject.id || ''); + } + + const meta = resp._source.kibanaSavedObjectMeta || {}; + delete resp._source.kibanaSavedObjectMeta; + + if (!config.indexPattern && savedObject._source.indexPattern) { + config.indexPattern = savedObject._source.indexPattern as IndexPattern; + delete savedObject._source.indexPattern; + } + + // assign the defaults to the response + _.defaults(savedObject._source, savedObject.defaults); + + // transform the source using _deserializers + _.forOwn(mapping, (fieldMapping, fieldName) => { + if (fieldMapping._deserialize && typeof fieldName === 'string') { + savedObject._source[fieldName] = fieldMapping._deserialize( + savedObject._source[fieldName] as string + ); + } + }); + + // Give obj all of the values in _source.fields + _.assign(savedObject, savedObject._source); + savedObject.lastSavedTitle = savedObject.title; + + try { + await parseSearchSource(savedObject, esType, meta.searchSourceJSON, resp.references); + await hydrateIndexPattern(); + if (injectReferences && resp.references && resp.references.length > 0) { + injectReferences(savedObject, resp.references); + } + if (typeof config.afterESResp === 'function') { + await config.afterESResp.call(savedObject); + } + return savedObject; + } catch (e) { + // eslint-disable-next-line no-console + console.error(e); + throw e; + } +} diff --git a/src/legacy/ui/public/saved_objects/helpers/build_saved_object.ts b/src/legacy/ui/public/saved_objects/helpers/build_saved_object.ts new file mode 100644 index 000000000000..a436f70f31ff --- /dev/null +++ b/src/legacy/ui/public/saved_objects/helpers/build_saved_object.ts @@ -0,0 +1,126 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import _ from 'lodash'; +import { SearchSource } from 'ui/courier'; +import { hydrateIndexPattern } from './hydrate_index_pattern'; +import { intializeSavedObject } from './initialize_saved_object'; +import { serializeSavedObject } from './serialize_saved_object'; + +import { + EsResponse, + SavedObject, + SavedObjectConfig, + SavedObjectKibanaServices, + SavedObjectSaveOpts, +} from '../types'; +import { applyESResp } from './apply_es_resp'; +import { saveSavedObject } from './save_saved_object'; + +export function buildSavedObject( + savedObject: SavedObject, + config: SavedObjectConfig = {}, + services: SavedObjectKibanaServices +) { + const { indexPatterns, savedObjectsClient } = services; + // type name for this object, used as the ES-type + const esType = config.type || ''; + + savedObject.getDisplayName = () => esType; + + // NOTE: this.type (not set in this file, but somewhere else) is the sub type, e.g. 'area' or + // 'data table', while esType is the more generic type - e.g. 'visualization' or 'saved search'. + savedObject.getEsType = () => esType; + + /** + * Flips to true during a save operation, and back to false once the save operation + * completes. + * @type {boolean} + */ + savedObject.isSaving = false; + savedObject.defaults = config.defaults || {}; + // optional search source which this object configures + savedObject.searchSource = config.searchSource ? new SearchSource() : undefined; + // the id of the document + savedObject.id = config.id || void 0; + // the migration version of the document, should only be set on imports + savedObject.migrationVersion = config.migrationVersion; + // Whether to create a copy when the object is saved. This should eventually go away + // in favor of a better rename/save flow. + savedObject.copyOnSave = false; + + /** + * After creation or fetching from ES, ensure that the searchSources index indexPattern + * is an bonafide IndexPattern object. + * + * @return {Promise} + */ + savedObject.hydrateIndexPattern = (id?: string) => + hydrateIndexPattern(id || '', savedObject, indexPatterns, config); + /** + * Asynchronously initialize this object - will only run + * once even if called multiple times. + * + * @return {Promise} + * @resolved {SavedObject} + */ + savedObject.init = _.once(() => intializeSavedObject(savedObject, savedObjectsClient, config)); + + savedObject.applyESResp = (resp: EsResponse) => applyESResp(resp, savedObject, config); + + /** + * Serialize this object + * @return {Object} + */ + savedObject._serialize = () => serializeSavedObject(savedObject, config); + + /** + * Returns true if the object's original title has been changed. New objects return false. + * @return {boolean} + */ + savedObject.isTitleChanged = () => + savedObject._source && savedObject._source.title !== savedObject.title; + + savedObject.creationOpts = (opts: Record = {}) => ({ + id: savedObject.id, + migrationVersion: savedObject.migrationVersion, + ...opts, + }); + + savedObject.save = async (opts: SavedObjectSaveOpts) => { + try { + const result = await saveSavedObject(savedObject, config, opts, services); + return Promise.resolve(result); + } catch (e) { + return Promise.reject(e); + } + }; + + savedObject.destroy = () => {}; + + /** + * Delete this object from Elasticsearch + * @return {promise} + */ + savedObject.delete = () => { + if (!savedObject.id) { + return Promise.reject(new Error('Deleting a saved Object requires type and id')); + } + return savedObjectsClient.delete(esType, savedObject.id); + }; +} diff --git a/src/legacy/ui/public/saved_objects/helpers/check_for_duplicate_title.ts b/src/legacy/ui/public/saved_objects/helpers/check_for_duplicate_title.ts new file mode 100644 index 000000000000..5c1c1d0d9a85 --- /dev/null +++ b/src/legacy/ui/public/saved_objects/helpers/check_for_duplicate_title.ts @@ -0,0 +1,69 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { SavedObject, SavedObjectKibanaServices } from '../types'; +import { findObjectByTitle } from './find_object_by_title'; +import { SAVE_DUPLICATE_REJECTED } from '../constants'; +import { displayDuplicateTitleConfirmModal } from './display_duplicate_title_confirm_modal'; + +/** + * check for an existing SavedObject with the same title in ES + * returns Promise when it's no duplicate, or the modal displaying the warning + * that's there's a duplicate is confirmed, else it returns a rejected Promise + * @param savedObject + * @param isTitleDuplicateConfirmed + * @param onTitleDuplicate + * @param services + */ +export async function checkForDuplicateTitle( + savedObject: SavedObject, + isTitleDuplicateConfirmed: boolean, + onTitleDuplicate: (() => void) | undefined, + services: SavedObjectKibanaServices +): Promise { + const { savedObjectsClient, overlays } = services; + // Don't check for duplicates if user has already confirmed save with duplicate title + if (isTitleDuplicateConfirmed) { + return true; + } + + // Don't check if the user isn't updating the title, otherwise that would become very annoying to have + // to confirm the save every time, except when copyOnSave is true, then we do want to check. + if (savedObject.title === savedObject.lastSavedTitle && !savedObject.copyOnSave) { + return true; + } + + const duplicate = await findObjectByTitle( + savedObjectsClient, + savedObject.getEsType(), + savedObject.title + ); + + if (!duplicate || duplicate.id === savedObject.id) { + return true; + } + + if (onTitleDuplicate) { + onTitleDuplicate(); + return Promise.reject(new Error(SAVE_DUPLICATE_REJECTED)); + } + + // TODO: make onTitleDuplicate a required prop and remove UI components from this class + // Need to leave here until all users pass onTitleDuplicate. + return displayDuplicateTitleConfirmModal(savedObject, overlays); +} diff --git a/src/legacy/ui/public/saved_objects/helpers/confirm_modal_promise.tsx b/src/legacy/ui/public/saved_objects/helpers/confirm_modal_promise.tsx new file mode 100644 index 000000000000..1e0a4f4ebe47 --- /dev/null +++ b/src/legacy/ui/public/saved_objects/helpers/confirm_modal_promise.tsx @@ -0,0 +1,59 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import { OverlayStart } from 'kibana/public'; +import { i18n } from '@kbn/i18n'; +import { EuiConfirmModal } from '@elastic/eui'; +import { toMountPoint } from '../../../../../plugins/kibana_react/public'; + +export function confirmModalPromise( + message = '', + title = '', + confirmBtnText = '', + overlays: OverlayStart +): Promise { + return new Promise((resolve, reject) => { + const cancelButtonText = i18n.translate( + 'common.ui.savedObjects.confirmModal.cancelButtonLabel', + { + defaultMessage: 'Cancel', + } + ); + + const modal = overlays.openModal( + toMountPoint( + { + modal.close(); + reject(); + }} + onConfirm={() => { + modal.close(); + resolve(true); + }} + confirmButtonText={confirmBtnText} + cancelButtonText={cancelButtonText} + title={title} + > + {message} + + ) + ); + }); +} diff --git a/src/legacy/ui/public/saved_objects/helpers/create_source.ts b/src/legacy/ui/public/saved_objects/helpers/create_source.ts new file mode 100644 index 000000000000..1818671ceceb --- /dev/null +++ b/src/legacy/ui/public/saved_objects/helpers/create_source.ts @@ -0,0 +1,85 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import _ from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { SavedObject, SavedObjectKibanaServices } from 'ui/saved_objects/types'; +import { SavedObjectAttributes } from 'kibana/public'; +import { OVERWRITE_REJECTED } from 'ui/saved_objects/constants'; +import { confirmModalPromise } from './confirm_modal_promise'; + +/** + * Attempts to create the current object using the serialized source. If an object already + * exists, a warning message requests an overwrite confirmation. + * @param source - serialized version of this object (return value from this._serialize()) + * What will be indexed into elasticsearch. + * @param savedObject - savedObject + * @param esType - type of the saved object + * @param options - options to pass to the saved object create method + * @param services - provides Kibana services savedObjectsClient and overlays + * @returns {Promise} - A promise that is resolved with the objects id if the object is + * successfully indexed. If the overwrite confirmation was rejected, an error is thrown with + * a confirmRejected = true parameter so that case can be handled differently than + * a create or index error. + * @resolved {SavedObject} + */ +export async function createSource( + source: SavedObjectAttributes, + savedObject: SavedObject, + esType: string, + options = {}, + services: SavedObjectKibanaServices +) { + const { savedObjectsClient, overlays } = services; + try { + return await savedObjectsClient.create(esType, source, options); + } catch (err) { + // record exists, confirm overwriting + if (_.get(err, 'res.status') === 409) { + const confirmMessage = i18n.translate( + 'common.ui.savedObjects.confirmModal.overwriteConfirmationMessage', + { + defaultMessage: 'Are you sure you want to overwrite {title}?', + values: { title: savedObject.title }, + } + ); + + const title = i18n.translate('common.ui.savedObjects.confirmModal.overwriteTitle', { + defaultMessage: 'Overwrite {name}?', + values: { name: savedObject.getDisplayName() }, + }); + const confirmButtonText = i18n.translate( + 'common.ui.savedObjects.confirmModal.overwriteButtonLabel', + { + defaultMessage: 'Overwrite', + } + ); + + return confirmModalPromise(confirmMessage, title, confirmButtonText, overlays) + .then(() => + savedObjectsClient.create( + esType, + source, + savedObject.creationOpts({ overwrite: true, ...options }) + ) + ) + .catch(() => Promise.reject(new Error(OVERWRITE_REJECTED))); + } + return await Promise.reject(err); + } +} diff --git a/src/legacy/ui/public/saved_objects/helpers/display_duplicate_title_confirm_modal.ts b/src/legacy/ui/public/saved_objects/helpers/display_duplicate_title_confirm_modal.ts new file mode 100644 index 000000000000..36882db72d56 --- /dev/null +++ b/src/legacy/ui/public/saved_objects/helpers/display_duplicate_title_confirm_modal.ts @@ -0,0 +1,49 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { i18n } from '@kbn/i18n'; +import { OverlayStart } from 'kibana/public'; +import { SAVE_DUPLICATE_REJECTED } from '../constants'; +import { confirmModalPromise } from './confirm_modal_promise'; +import { SavedObject } from '../types'; + +export function displayDuplicateTitleConfirmModal( + savedObject: SavedObject, + overlays: OverlayStart +): Promise { + const confirmMessage = i18n.translate( + 'common.ui.savedObjects.confirmModal.saveDuplicateConfirmationMessage', + { + defaultMessage: `A {name} with the title '{title}' already exists. Would you like to save anyway?`, + values: { title: savedObject.title, name: savedObject.getDisplayName() }, + } + ); + + const confirmButtonText = i18n.translate( + 'common.ui.savedObjects.confirmModal.saveDuplicateButtonLabel', + { + defaultMessage: 'Save {name}', + values: { name: savedObject.getDisplayName() }, + } + ); + try { + return confirmModalPromise(confirmMessage, '', confirmButtonText, overlays); + } catch (_) { + return Promise.reject(new Error(SAVE_DUPLICATE_REJECTED)); + } +} diff --git a/src/legacy/ui/public/saved_objects/find_object_by_title.ts b/src/legacy/ui/public/saved_objects/helpers/find_object_by_title.ts similarity index 75% rename from src/legacy/ui/public/saved_objects/find_object_by_title.ts rename to src/legacy/ui/public/saved_objects/helpers/find_object_by_title.ts index d6f11bcb8095..373800f57662 100644 --- a/src/legacy/ui/public/saved_objects/find_object_by_title.ts +++ b/src/legacy/ui/public/saved_objects/helpers/find_object_by_title.ts @@ -17,7 +17,6 @@ * under the License. */ -import { find } from 'lodash'; import { SavedObjectAttributes } from 'src/core/server'; import { SavedObjectsClientContract } from 'src/core/public'; import { SimpleSavedObject } from 'src/core/public'; @@ -30,30 +29,23 @@ import { SimpleSavedObject } from 'src/core/public'; * @param title {string} * @returns {Promise} */ -export function findObjectByTitle( +export async function findObjectByTitle( savedObjectsClient: SavedObjectsClientContract, type: string, title: string ): Promise | void> { if (!title) { - return Promise.resolve(); + return; } // Elastic search will return the most relevant results first, which means exact matches should come // first, and so we shouldn't need to request everything. Using 10 just to be on the safe side. - return savedObjectsClient - .find({ - type, - perPage: 10, - search: `"${title}"`, - searchFields: ['title'], - fields: ['title'], - }) - .then(response => { - const match = find(response.savedObjects, obj => { - return obj.get('title').toLowerCase() === title.toLowerCase(); - }); - - return match; - }); + const response = await savedObjectsClient.find({ + type, + perPage: 10, + search: `"${title}"`, + searchFields: ['title'], + fields: ['title'], + }); + return response.savedObjects.find(obj => obj.get('title').toLowerCase() === title.toLowerCase()); } diff --git a/src/legacy/ui/public/saved_objects/helpers/hydrate_index_pattern.ts b/src/legacy/ui/public/saved_objects/helpers/hydrate_index_pattern.ts new file mode 100644 index 000000000000..a78b3f97e884 --- /dev/null +++ b/src/legacy/ui/public/saved_objects/helpers/hydrate_index_pattern.ts @@ -0,0 +1,55 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { SavedObject, SavedObjectConfig } from '../types'; +import { IndexPatternsContract } from '../../../../../plugins/data/public'; + +/** + * After creation or fetching from ES, ensure that the searchSources index indexPattern + * is an bonafide IndexPattern object. + * + * @return {Promise} + */ +export async function hydrateIndexPattern( + id: string, + savedObject: SavedObject, + indexPatterns: IndexPatternsContract, + config: SavedObjectConfig +) { + const clearSavedIndexPattern = !!config.clearSavedIndexPattern; + const indexPattern = config.indexPattern; + + if (!savedObject.searchSource) { + return null; + } + + if (clearSavedIndexPattern) { + savedObject.searchSource!.setField('index', undefined); + return null; + } + + const index = id || indexPattern || savedObject.searchSource!.getOwnField('index'); + + if (typeof index !== 'string' || !index) { + return null; + } + + const indexObj = await indexPatterns.get(index); + savedObject.searchSource!.setField('index', indexObj); + return indexObj; +} diff --git a/src/legacy/ui/public/saved_objects/helpers/initialize_saved_object.ts b/src/legacy/ui/public/saved_objects/helpers/initialize_saved_object.ts new file mode 100644 index 000000000000..c5ea31e0784a --- /dev/null +++ b/src/legacy/ui/public/saved_objects/helpers/initialize_saved_object.ts @@ -0,0 +1,59 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import _ from 'lodash'; +import { SavedObjectsClientContract } from 'kibana/public'; +import { SavedObject, SavedObjectConfig } from '../types'; + +/** + * Initialize saved object + */ +export async function intializeSavedObject( + savedObject: SavedObject, + savedObjectsClient: SavedObjectsClientContract, + config: SavedObjectConfig +) { + const esType = config.type; + // ensure that the esType is defined + if (!esType) throw new Error('You must define a type name to use SavedObject objects.'); + + if (!savedObject.id) { + // just assign the defaults and be done + _.assign(savedObject, savedObject.defaults); + await savedObject.hydrateIndexPattern!(); + if (typeof config.afterESResp === 'function') { + await config.afterESResp.call(savedObject); + } + return savedObject; + } + + const resp = await savedObjectsClient.get(esType, savedObject.id); + const respMapped = { + _id: resp.id, + _type: resp.type, + _source: _.cloneDeep(resp.attributes), + references: resp.references, + found: !!resp._version, + }; + await savedObject.applyESResp(respMapped); + if (typeof config.init === 'function') { + await config.init.call(savedObject); + } + + return savedObject; +} diff --git a/src/legacy/ui/public/saved_objects/helpers/parse_search_source.ts b/src/legacy/ui/public/saved_objects/helpers/parse_search_source.ts new file mode 100644 index 000000000000..8c52b7cfa0db --- /dev/null +++ b/src/legacy/ui/public/saved_objects/helpers/parse_search_source.ts @@ -0,0 +1,97 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import _ from 'lodash'; +import { migrateLegacyQuery } from 'ui/utils/migrate_legacy_query'; +import { SavedObject } from '../types'; +import { InvalidJSONProperty } from '../../../../../plugins/kibana_utils/public'; + +export function parseSearchSource( + savedObject: SavedObject, + esType: string, + searchSourceJson: string, + references: any[] +) { + if (!savedObject.searchSource) return; + + // if we have a searchSource, set its values based on the searchSourceJson field + let searchSourceValues: Record; + try { + searchSourceValues = JSON.parse(searchSourceJson); + } catch (e) { + throw new InvalidJSONProperty( + `Invalid JSON in ${esType} "${savedObject.id}". ${e.message} JSON: ${searchSourceJson}` + ); + } + + // This detects a scenario where documents with invalid JSON properties have been imported into the saved object index. + // (This happened in issue #20308) + if (!searchSourceValues || typeof searchSourceValues !== 'object') { + throw new InvalidJSONProperty(`Invalid searchSourceJSON in ${esType} "${savedObject.id}".`); + } + + // Inject index id if a reference is saved + if (searchSourceValues.indexRefName) { + const reference = references.find( + (ref: Record) => ref.name === searchSourceValues.indexRefName + ); + if (!reference) { + throw new Error( + `Could not find reference for ${ + searchSourceValues.indexRefName + } on ${savedObject.getEsType()} ${savedObject.id}` + ); + } + searchSourceValues.index = reference.id; + delete searchSourceValues.indexRefName; + } + + if (searchSourceValues.filter) { + searchSourceValues.filter.forEach((filterRow: any) => { + if (!filterRow.meta || !filterRow.meta.indexRefName) { + return; + } + const reference = references.find((ref: any) => ref.name === filterRow.meta.indexRefName); + if (!reference) { + throw new Error( + `Could not find reference for ${ + filterRow.meta.indexRefName + } on ${savedObject.getEsType()}` + ); + } + filterRow.meta.index = reference.id; + delete filterRow.meta.indexRefName; + }); + } + + const searchSourceFields = savedObject.searchSource.getFields(); + const fnProps = _.transform( + searchSourceFields, + function(dynamic: Record, val: any, name: string | undefined) { + if (_.isFunction(val) && name) dynamic[name] = val; + }, + {} + ); + + savedObject.searchSource.setFields(_.defaults(searchSourceValues, fnProps)); + const query = savedObject.searchSource.getOwnField('query'); + + if (typeof query !== 'undefined') { + savedObject.searchSource.setField('query', migrateLegacyQuery(query)); + } +} diff --git a/src/legacy/ui/public/saved_objects/helpers/save_saved_object.ts b/src/legacy/ui/public/saved_objects/helpers/save_saved_object.ts new file mode 100644 index 000000000000..bd6daa1832a2 --- /dev/null +++ b/src/legacy/ui/public/saved_objects/helpers/save_saved_object.ts @@ -0,0 +1,128 @@ +/* + * 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 { + SavedObject, + SavedObjectConfig, + SavedObjectKibanaServices, + SavedObjectSaveOpts, +} from '../types'; +import { OVERWRITE_REJECTED, SAVE_DUPLICATE_REJECTED } from '../constants'; +import { createSource } from './create_source'; +import { checkForDuplicateTitle } from './check_for_duplicate_title'; + +/** + * @param error {Error} the error + * @return {boolean} + */ +function isErrorNonFatal(error: { message: string }) { + if (!error) return false; + return error.message === OVERWRITE_REJECTED || error.message === SAVE_DUPLICATE_REJECTED; +} + +/** + * Saves this object. + * + * @param {string} [esType] + * @param {SavedObject} [savedObject] + * @param {SavedObjectConfig} [config] + * @param {object} [options={}] + * @property {boolean} [options.confirmOverwrite=false] - If true, attempts to create the source so it + * can confirm an overwrite if a document with the id already exists. + * @property {boolean} [options.isTitleDuplicateConfirmed=false] - If true, save allowed with duplicate title + * @property {func} [options.onTitleDuplicate] - function called if duplicate title exists. + * When not provided, confirm modal will be displayed asking user to confirm or cancel save. + * @param {SavedObjectKibanaServices} [services] + * @return {Promise} + * @resolved {String} - The id of the doc + */ +export async function saveSavedObject( + savedObject: SavedObject, + config: SavedObjectConfig, + { + confirmOverwrite = false, + isTitleDuplicateConfirmed = false, + onTitleDuplicate, + }: SavedObjectSaveOpts = {}, + services: SavedObjectKibanaServices +): Promise { + const { savedObjectsClient, chrome } = services; + + const esType = config.type || ''; + const extractReferences = config.extractReferences; + // Save the original id in case the save fails. + const originalId = savedObject.id; + // Read https://github.com/elastic/kibana/issues/9056 and + // https://github.com/elastic/kibana/issues/9012 for some background into why this copyOnSave variable + // exists. + // The goal is to move towards a better rename flow, but since our users have been conditioned + // to expect a 'save as' flow during a rename, we are keeping the logic the same until a better + // UI/UX can be worked out. + if (savedObject.copyOnSave) { + delete savedObject.id; + } + + // Here we want to extract references and set them within "references" attribute + let { attributes, references } = savedObject._serialize(); + if (extractReferences) { + ({ attributes, references } = extractReferences({ attributes, references })); + } + if (!references) throw new Error('References not returned from extractReferences'); + + try { + await checkForDuplicateTitle( + savedObject, + isTitleDuplicateConfirmed, + onTitleDuplicate, + services + ); + savedObject.isSaving = true; + const resp = confirmOverwrite + ? await createSource( + attributes, + savedObject, + esType, + savedObject.creationOpts({ references }), + services + ) + : await savedObjectsClient.create( + esType, + attributes, + savedObject.creationOpts({ references, overwrite: true }) + ); + + savedObject.id = resp.id; + if (savedObject.showInRecentlyAccessed && savedObject.getFullPath) { + chrome.recentlyAccessed.add( + savedObject.getFullPath(), + savedObject.title, + String(savedObject.id) + ); + } + savedObject.isSaving = false; + savedObject.lastSavedTitle = savedObject.title; + return savedObject.id; + } catch (err) { + savedObject.isSaving = false; + savedObject.id = originalId; + if (isErrorNonFatal(err)) { + return ''; + } + return Promise.reject(err); + } +} diff --git a/src/legacy/ui/public/saved_objects/helpers/serialize_saved_object.ts b/src/legacy/ui/public/saved_objects/helpers/serialize_saved_object.ts new file mode 100644 index 000000000000..ca780f8f9584 --- /dev/null +++ b/src/legacy/ui/public/saved_objects/helpers/serialize_saved_object.ts @@ -0,0 +1,98 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import _ from 'lodash'; +import angular from 'angular'; +import { SavedObject, SavedObjectConfig } from '../types'; +import { expandShorthand } from '../../../../../plugins/kibana_utils/public'; + +export function serializeSavedObject(savedObject: SavedObject, config: SavedObjectConfig) { + // mapping definition for the fields that this object will expose + const mapping = expandShorthand(config.mapping); + const attributes = {} as Record; + const references = []; + + _.forOwn(mapping, (fieldMapping, fieldName) => { + if (typeof fieldName !== 'string') { + return; + } + // @ts-ignore + const savedObjectFieldVal = savedObject[fieldName]; + if (savedObjectFieldVal != null) { + attributes[fieldName] = fieldMapping._serialize + ? fieldMapping._serialize(savedObjectFieldVal) + : savedObjectFieldVal; + } + }); + + if (savedObject.searchSource) { + let searchSourceFields: Record = _.omit(savedObject.searchSource.getFields(), [ + 'sort', + 'size', + ]); + if (searchSourceFields.index) { + // searchSourceFields.index will normally be an IndexPattern, but can be a string in two scenarios: + // (1) `init()` (and by extension `hydrateIndexPattern()`) hasn't been called on Saved Object + // (2) The IndexPattern doesn't exist, so we fail to resolve it in `hydrateIndexPattern()` + const indexId = + typeof searchSourceFields.index === 'string' + ? searchSourceFields.index + : searchSourceFields.index.id; + const refName = 'kibanaSavedObjectMeta.searchSourceJSON.index'; + references.push({ + name: refName, + type: 'index-pattern', + id: indexId, + }); + searchSourceFields = { + ...searchSourceFields, + indexRefName: refName, + index: undefined, + }; + } + if (searchSourceFields.filter) { + searchSourceFields = { + ...searchSourceFields, + filter: searchSourceFields.filter.map((filterRow: any, i: number) => { + if (!filterRow.meta || !filterRow.meta.index) { + return filterRow; + } + const refName = `kibanaSavedObjectMeta.searchSourceJSON.filter[${i}].meta.index`; + references.push({ + name: refName, + type: 'index-pattern', + id: filterRow.meta.index, + }); + return { + ...filterRow, + meta: { + ...filterRow.meta, + indexRefName: refName, + index: undefined, + }, + }; + }), + }; + } + attributes.kibanaSavedObjectMeta = { + searchSourceJSON: angular.toJson(searchSourceFields), + }; + } + + return { attributes, references }; +} diff --git a/src/legacy/ui/public/saved_objects/index.ts b/src/legacy/ui/public/saved_objects/index.ts index 8076213f62e9..3c77a02c608c 100644 --- a/src/legacy/ui/public/saved_objects/index.ts +++ b/src/legacy/ui/public/saved_objects/index.ts @@ -19,6 +19,5 @@ export { SavedObjectRegistryProvider } from './saved_object_registry'; export { SavedObjectsClientProvider } from './saved_objects_client_provider'; -// @ts-ignore export { SavedObjectLoader } from './saved_object_loader'; -export { findObjectByTitle } from './find_object_by_title'; +export { findObjectByTitle } from './helpers/find_object_by_title'; diff --git a/src/legacy/ui/public/saved_objects/saved_object.js b/src/legacy/ui/public/saved_objects/saved_object.js deleted file mode 100644 index 1db651ad9308..000000000000 --- a/src/legacy/ui/public/saved_objects/saved_object.js +++ /dev/null @@ -1,541 +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. - */ - -/** - * @name SavedObject - * - * NOTE: SavedObject seems to track a reference to an object in ES, - * and surface methods for CRUD functionality (save and delete). This seems - * similar to how Backbone Models work. - * - * This class seems to interface with ES primarily through the es Angular - * service and the saved object api. - */ - -import angular from 'angular'; -import _ from 'lodash'; - - -import { InvalidJSONProperty, SavedObjectNotFound, expandShorthand } from '../../../../plugins/kibana_utils/public'; - -import { SearchSource } from '../courier'; -import { findObjectByTitle } from './find_object_by_title'; -import { SavedObjectsClientProvider } from './saved_objects_client_provider'; -import { migrateLegacyQuery } from '../utils/migrate_legacy_query'; -import { npStart } from 'ui/new_platform'; -import { i18n } from '@kbn/i18n'; - -/** - * An error message to be used when the user rejects a confirm overwrite. - * @type {string} - */ -const OVERWRITE_REJECTED = i18n.translate('common.ui.savedObjects.overwriteRejectedDescription', { - defaultMessage: 'Overwrite confirmation was rejected' -}); - -/** - * An error message to be used when the user rejects a confirm save with duplicate title. - * @type {string} - */ -const SAVE_DUPLICATE_REJECTED = i18n.translate('common.ui.savedObjects.saveDuplicateRejectedDescription', { - defaultMessage: 'Save with duplicate title confirmation was rejected' -}); - -/** - * @param error {Error} the error - * @return {boolean} - */ -function isErrorNonFatal(error) { - if (!error) return false; - return error.message === OVERWRITE_REJECTED || error.message === SAVE_DUPLICATE_REJECTED; -} - -export function SavedObjectProvider(Promise, Private, confirmModalPromise, indexPatterns) { - const savedObjectsClient = Private(SavedObjectsClientProvider); - - /** - * The SavedObject class is a base class for saved objects loaded from the server and - * provides additional functionality besides loading/saving/deleting/etc. - * - * It is overloaded and configured to provide type-aware functionality. - * To just retrieve the attributes of saved objects, it is recommended to use SavedObjectLoader - * which returns instances of SimpleSavedObject which don't introduce additional type-specific complexity. - * @param {*} config - */ - function SavedObject(config) { - if (!_.isObject(config)) config = {}; - - /************ - * Initialize config vars - ************/ - - // type name for this object, used as the ES-type - const esType = config.type; - - this.getDisplayName = function () { - return esType; - }; - - // NOTE: this.type (not set in this file, but somewhere else) is the sub type, e.g. 'area' or - // 'data table', while esType is the more generic type - e.g. 'visualization' or 'saved search'. - this.getEsType = function () { - return esType; - }; - - /** - * Flips to true during a save operation, and back to false once the save operation - * completes. - * @type {boolean} - */ - this.isSaving = false; - this.defaults = config.defaults || {}; - - // mapping definition for the fields that this object will expose - const mapping = expandShorthand(config.mapping); - - const afterESResp = config.afterESResp || _.noop; - const customInit = config.init || _.noop; - const extractReferences = config.extractReferences; - const injectReferences = config.injectReferences; - - // optional search source which this object configures - this.searchSource = config.searchSource ? new SearchSource() : undefined; - - // the id of the document - this.id = config.id || void 0; - - // the migration version of the document, should only be set on imports - this.migrationVersion = config.migrationVersion; - - // Whether to create a copy when the object is saved. This should eventually go away - // in favor of a better rename/save flow. - this.copyOnSave = false; - - const parseSearchSource = (searchSourceJson, references) => { - if (!this.searchSource) return; - - // if we have a searchSource, set its values based on the searchSourceJson field - let searchSourceValues; - try { - searchSourceValues = JSON.parse(searchSourceJson); - } catch (e) { - throw new InvalidJSONProperty( - `Invalid JSON in ${esType} "${this.id}". ${e.message} JSON: ${searchSourceJson}` - ); - } - - // This detects a scenario where documents with invalid JSON properties have been imported into the saved object index. - // (This happened in issue #20308) - if (!searchSourceValues || typeof searchSourceValues !== 'object') { - throw new InvalidJSONProperty(`Invalid searchSourceJSON in ${esType} "${this.id}".`); - } - - // Inject index id if a reference is saved - if (searchSourceValues.indexRefName) { - const reference = references.find(reference => reference.name === searchSourceValues.indexRefName); - if (!reference) { - throw new Error(`Could not find reference for ${searchSourceValues.indexRefName} on ${this.getEsType()} ${this.id}`); - } - searchSourceValues.index = reference.id; - delete searchSourceValues.indexRefName; - } - - if (searchSourceValues.filter) { - searchSourceValues.filter.forEach((filterRow) => { - if (!filterRow.meta || !filterRow.meta.indexRefName) { - return; - } - const reference = references.find(reference => reference.name === filterRow.meta.indexRefName); - if (!reference) { - throw new Error(`Could not find reference for ${filterRow.meta.indexRefName} on ${this.getEsType()}`); - } - filterRow.meta.index = reference.id; - delete filterRow.meta.indexRefName; - }); - } - - const searchSourceFields = this.searchSource.getFields(); - const fnProps = _.transform(searchSourceFields, function (dynamic, val, name) { - if (_.isFunction(val)) dynamic[name] = val; - }, {}); - - this.searchSource.setFields(_.defaults(searchSourceValues, fnProps)); - - if (!_.isUndefined(this.searchSource.getOwnField('query'))) { - this.searchSource.setField('query', migrateLegacyQuery(this.searchSource.getOwnField('query'))); - } - }; - - /** - * After creation or fetching from ES, ensure that the searchSources index indexPattern - * is an bonafide IndexPattern object. - * - * @return {Promise} - */ - this.hydrateIndexPattern = (id) => { - if (!this.searchSource) { - return Promise.resolve(null); - } - - if (config.clearSavedIndexPattern) { - this.searchSource.setField('index', null); - return Promise.resolve(null); - } - - let index = id || config.indexPattern || this.searchSource.getOwnField('index'); - - if (!index) { - return Promise.resolve(null); - } - - // If index is not an IndexPattern object at this point, then it's a string id of an index. - if (typeof index === 'string') { - index = indexPatterns.get(index); - } - - // At this point index will either be an IndexPattern, if cached, or a promise that - // will return an IndexPattern, if not cached. - return Promise.resolve(index).then(indexPattern => { - this.searchSource.setField('index', indexPattern); - }); - }; - - /** - * Asynchronously initialize this object - will only run - * once even if called multiple times. - * - * @return {Promise} - * @resolved {SavedObject} - */ - this.init = _.once(() => { - // ensure that the esType is defined - if (!esType) throw new Error('You must define a type name to use SavedObject objects.'); - - return Promise.resolve() - .then(() => { - // If there is not id, then there is no document to fetch from elasticsearch - if (!this.id) { - // just assign the defaults and be done - _.assign(this, this.defaults); - return this.hydrateIndexPattern().then(() => { - return afterESResp.call(this); - }); - } - - // fetch the object from ES - return savedObjectsClient.get(esType, this.id) - .then(resp => { - // temporary compatability for savedObjectsClient - return { - _id: resp.id, - _type: resp.type, - _source: _.cloneDeep(resp.attributes), - references: resp.references, - found: resp._version ? true : false - }; - }) - .then(this.applyESResp) - .catch(this.applyEsResp); - }) - .then(() => customInit.call(this)) - .then(() => this); - }); - - - this.applyESResp = (resp) => { - this._source = _.cloneDeep(resp._source); - - if (resp.found != null && !resp.found) { - throw new SavedObjectNotFound(esType, this.id); - } - - const meta = resp._source.kibanaSavedObjectMeta || {}; - delete resp._source.kibanaSavedObjectMeta; - - if (!config.indexPattern && this._source.indexPattern) { - config.indexPattern = this._source.indexPattern; - delete this._source.indexPattern; - } - - // assign the defaults to the response - _.defaults(this._source, this.defaults); - - // transform the source using _deserializers - _.forOwn(mapping, (fieldMapping, fieldName) => { - if (fieldMapping._deserialize) { - this._source[fieldName] = fieldMapping._deserialize(this._source[fieldName], resp, fieldName, fieldMapping); - } - }); - - // Give obj all of the values in _source.fields - _.assign(this, this._source); - this.lastSavedTitle = this.title; - - return Promise.try(() => { - parseSearchSource(meta.searchSourceJSON, resp.references); - return this.hydrateIndexPattern(); - }).then(() => { - if (injectReferences && resp.references && resp.references.length > 0) { - injectReferences(this, resp.references); - } - return this; - }).then(() => { - return Promise.cast(afterESResp.call(this, resp)); - }); - }; - - /** - * Serialize this object - * - * @return {Object} - */ - this._serialize = () => { - const attributes = {}; - const references = []; - - _.forOwn(mapping, (fieldMapping, fieldName) => { - if (this[fieldName] != null) { - attributes[fieldName] = (fieldMapping._serialize) - ? fieldMapping._serialize(this[fieldName]) - : this[fieldName]; - } - }); - - if (this.searchSource) { - let searchSourceFields = _.omit(this.searchSource.getFields(), ['sort', 'size']); - if (searchSourceFields.index) { - // searchSourceFields.index will normally be an IndexPattern, but can be a string in two scenarios: - // (1) `init()` (and by extension `hydrateIndexPattern()`) hasn't been called on this Saved Object - // (2) The IndexPattern doesn't exist, so we fail to resolve it in `hydrateIndexPattern()` - const indexId = typeof (searchSourceFields.index) === 'string' ? searchSourceFields.index : searchSourceFields.index.id; - const refName = 'kibanaSavedObjectMeta.searchSourceJSON.index'; - references.push({ - name: refName, - type: 'index-pattern', - id: indexId, - }); - searchSourceFields = { - ...searchSourceFields, - indexRefName: refName, - index: undefined, - }; - } - if (searchSourceFields.filter) { - searchSourceFields = { - ...searchSourceFields, - filter: searchSourceFields.filter.map((filterRow, i) => { - if (!filterRow.meta || !filterRow.meta.index) { - return filterRow; - } - const refName = `kibanaSavedObjectMeta.searchSourceJSON.filter[${i}].meta.index`; - references.push({ - name: refName, - type: 'index-pattern', - id: filterRow.meta.index, - }); - return { - ...filterRow, - meta: { - ...filterRow.meta, - indexRefName: refName, - index: undefined, - } - }; - }), - }; - } - attributes.kibanaSavedObjectMeta = { - searchSourceJSON: angular.toJson(searchSourceFields) - }; - } - - return { attributes, references }; - }; - - /** - * Returns true if the object's original title has been changed. New objects return false. - * @return {boolean} - */ - this.isTitleChanged = () => { - return this._source && this._source.title !== this.title; - }; - - this.creationOpts = (opts = {}) => ({ - id: this.id, - migrationVersion: this.migrationVersion, - ...opts, - }); - - /** - * Attempts to create the current object using the serialized source. If an object already - * exists, a warning message requests an overwrite confirmation. - * @param source - serialized version of this object (return value from this._serialize()) - * What will be indexed into elasticsearch. - * @param options - options to pass to the saved object create method - * @returns {Promise} - A promise that is resolved with the objects id if the object is - * successfully indexed. If the overwrite confirmation was rejected, an error is thrown with - * a confirmRejected = true parameter so that case can be handled differently than - * a create or index error. - * @resolved {SavedObject} - */ - const createSource = (source, options = {}) => { - return savedObjectsClient.create(esType, source, options) - .catch(err => { - // record exists, confirm overwriting - if (_.get(err, 'res.status') === 409) { - const confirmMessage = i18n.translate('common.ui.savedObjects.confirmModal.overwriteConfirmationMessage', { - defaultMessage: 'Are you sure you want to overwrite {title}?', - values: { title: this.title } - }); - - return confirmModalPromise(confirmMessage, { - confirmButtonText: i18n.translate('common.ui.savedObjects.confirmModal.overwriteButtonLabel', { - defaultMessage: 'Overwrite', - }), - title: i18n.translate('common.ui.savedObjects.confirmModal.overwriteTitle', { - defaultMessage: 'Overwrite {name}?', - values: { name: this.getDisplayName() } - }), - }) - .then(() => savedObjectsClient.create(esType, source, this.creationOpts({ overwrite: true, ...options }))) - .catch(() => Promise.reject(new Error(OVERWRITE_REJECTED))); - } - return Promise.reject(err); - }); - }; - - const displayDuplicateTitleConfirmModal = () => { - const confirmMessage = i18n.translate('common.ui.savedObjects.confirmModal.saveDuplicateConfirmationMessage', { - defaultMessage: `A {name} with the title '{title}' already exists. Would you like to save anyway?`, - values: { title: this.title, name: this.getDisplayName() } - }); - - return confirmModalPromise(confirmMessage, { - confirmButtonText: i18n.translate('common.ui.savedObjects.confirmModal.saveDuplicateButtonLabel', { - defaultMessage: 'Save {name}', - values: { name: this.getDisplayName() } - }) - }) - .catch(() => Promise.reject(new Error(SAVE_DUPLICATE_REJECTED))); - }; - - const checkForDuplicateTitle = (isTitleDuplicateConfirmed, onTitleDuplicate) => { - // Don't check for duplicates if user has already confirmed save with duplicate title - if (isTitleDuplicateConfirmed) { - return Promise.resolve(); - } - - // Don't check if the user isn't updating the title, otherwise that would become very annoying to have - // to confirm the save every time, except when copyOnSave is true, then we do want to check. - if (this.title === this.lastSavedTitle && !this.copyOnSave) { - return Promise.resolve(); - } - - return findObjectByTitle(savedObjectsClient, this.getEsType(), this.title) - .then(duplicate => { - if (!duplicate) return true; - if (duplicate.id === this.id) return true; - - if (onTitleDuplicate) { - onTitleDuplicate(); - return Promise.reject(new Error(SAVE_DUPLICATE_REJECTED)); - } - - // TODO: make onTitleDuplicate a required prop and remove UI components from this class - // Need to leave here until all users pass onTitleDuplicate. - return displayDuplicateTitleConfirmModal(); - }); - }; - - /** - * Saves this object. - * - * @param {object} [options={}] - * @property {boolean} [options.confirmOverwrite=false] - If true, attempts to create the source so it - * can confirm an overwrite if a document with the id already exists. - * @property {boolean} [options.isTitleDuplicateConfirmed=false] - If true, save allowed with duplicate title - * @property {func} [options.onTitleDuplicate] - function called if duplicate title exists. - * When not provided, confirm modal will be displayed asking user to confirm or cancel save. - * @return {Promise} - * @resolved {String} - The id of the doc - */ - this.save = ({ confirmOverwrite = false, isTitleDuplicateConfirmed = false, onTitleDuplicate } = {}) => { - // Save the original id in case the save fails. - const originalId = this.id; - // Read https://github.com/elastic/kibana/issues/9056 and - // https://github.com/elastic/kibana/issues/9012 for some background into why this copyOnSave variable - // exists. - // The goal is to move towards a better rename flow, but since our users have been conditioned - // to expect a 'save as' flow during a rename, we are keeping the logic the same until a better - // UI/UX can be worked out. - if (this.copyOnSave) { - this.id = null; - } - - // Here we want to extract references and set them within "references" attribute - let { attributes, references } = this._serialize(); - if (extractReferences) { - ({ attributes, references } = extractReferences({ attributes, references })); - } - if (!references) throw new Error('References not returned from extractReferences'); - - this.isSaving = true; - - return checkForDuplicateTitle(isTitleDuplicateConfirmed, onTitleDuplicate) - .then(() => { - if (confirmOverwrite) { - return createSource(attributes, this.creationOpts({ references })); - } else { - return savedObjectsClient.create(esType, attributes, this.creationOpts({ references, overwrite: true })); - } - }) - .then((resp) => { - this.id = resp.id; - }) - .then(() => { - if (this.showInRecentlyAccessed && this.getFullPath) { - npStart.core.chrome.recentlyAccessed.add(this.getFullPath(), this.title, this.id); - } - this.isSaving = false; - this.lastSavedTitle = this.title; - return this.id; - }) - .catch((err) => { - this.isSaving = false; - this.id = originalId; - if (isErrorNonFatal(err)) { - return; - } - return Promise.reject(err); - }); - }; - - this.destroy = () => {}; - - /** - * Delete this object from Elasticsearch - * @return {promise} - */ - this.delete = () => { - return savedObjectsClient.delete(esType, this.id); - }; - } - - return SavedObject; -} diff --git a/src/legacy/ui/public/saved_objects/saved_object.ts b/src/legacy/ui/public/saved_objects/saved_object.ts new file mode 100644 index 000000000000..91182e67aac0 --- /dev/null +++ b/src/legacy/ui/public/saved_objects/saved_object.ts @@ -0,0 +1,63 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * @name SavedObject + * + * NOTE: SavedObject seems to track a reference to an object in ES, + * and surface methods for CRUD functionality (save and delete). This seems + * similar to how Backbone Models work. + * + * This class seems to interface with ES primarily through the es Angular + * service and the saved object api. + */ +import { npStart } from 'ui/new_platform'; +import { SavedObject, SavedObjectConfig, SavedObjectKibanaServices } from './types'; +import { buildSavedObject } from './helpers/build_saved_object'; + +export function createSavedObjectClass(services: SavedObjectKibanaServices) { + /** + * The SavedObject class is a base class for saved objects loaded from the server and + * provides additional functionality besides loading/saving/deleting/etc. + * + * It is overloaded and configured to provide type-aware functionality. + * To just retrieve the attributes of saved objects, it is recommended to use SavedObjectLoader + * which returns instances of SimpleSavedObject which don't introduce additional type-specific complexity. + * @param {*} config + */ + class SavedObjectClass { + constructor(config: SavedObjectConfig = {}) { + // @ts-ignore + const self: SavedObject = this; + buildSavedObject(self, config, services); + } + } + + return SavedObjectClass as new (config: SavedObjectConfig) => SavedObject; +} +// the old angular way, should be removed once no longer used +export function SavedObjectProvider() { + const services = { + savedObjectsClient: npStart.core.savedObjects.client, + indexPatterns: npStart.plugins.data.indexPatterns, + chrome: npStart.core.chrome, + overlays: npStart.core.overlays, + }; + return createSavedObjectClass(services); +} diff --git a/src/legacy/ui/public/saved_objects/saved_object_loader.js b/src/legacy/ui/public/saved_objects/saved_object_loader.ts similarity index 60% rename from src/legacy/ui/public/saved_objects/saved_object_loader.js rename to src/legacy/ui/public/saved_objects/saved_object_loader.ts index 434ce0d8b0ca..eb880ce5380c 100644 --- a/src/legacy/ui/public/saved_objects/saved_object_loader.js +++ b/src/legacy/ui/public/saved_objects/saved_object_loader.ts @@ -16,7 +16,8 @@ * specific language governing permissions and limitations * under the License. */ - +import { SavedObject } from 'ui/saved_objects/types'; +import { ChromeStart, SavedObjectsClientContract, SavedObjectsFindOptions } from 'kibana/public'; import { StringUtils } from '../utils/string_utils'; /** @@ -28,20 +29,25 @@ import { StringUtils } from '../utils/string_utils'; * to avoid pulling in extra functionality which isn't used. */ export class SavedObjectLoader { - constructor(SavedObjectClass, kbnUrl, chrome, savedObjectClient) { + private readonly Class: (id: string) => SavedObject; + public type: string; + public lowercaseType: string; + public loaderProperties: Record; + + constructor( + SavedObjectClass: any, + private readonly savedObjectsClient: SavedObjectsClientContract, + private readonly chrome: ChromeStart + ) { this.type = SavedObjectClass.type; this.Class = SavedObjectClass; this.lowercaseType = this.type.toLowerCase(); - this.kbnUrl = kbnUrl; - this.chrome = chrome; this.loaderProperties = { - name: `${ this.lowercaseType }s`, + name: `${this.lowercaseType}s`, noun: StringUtils.upperFirst(this.type), - nouns: `${ this.lowercaseType }s`, + nouns: `${this.lowercaseType}s`, }; - - this.savedObjectsClient = savedObjectClient; } /** @@ -50,27 +56,38 @@ export class SavedObjectLoader { * @param id * @returns {Promise} */ - get(id) { - return (new this.Class(id)).init(); + async get(id: string) { + // @ts-ignore + const obj = new this.Class(id); + return obj.init(); } - urlFor(id) { - return this.kbnUrl.eval(`#/${ this.lowercaseType }/{{id}}`, { id: id }); + urlFor(id: string) { + return `#/${this.lowercaseType}/${encodeURIComponent(id)}`; } - delete(ids) { - ids = !Array.isArray(ids) ? [ids] : ids; + async delete(ids: string | string[]) { + const idsUsed = !Array.isArray(ids) ? [ids] : ids; - const deletions = ids.map(id => { + const deletions = idsUsed.map(id => { + // @ts-ignore const savedObject = new this.Class(id); return savedObject.delete(); }); + await Promise.all(deletions); - return Promise.all(deletions).then(() => { - if (this.chrome) { - this.chrome.untrackNavLinksForDeletedSavedObjects(ids); - } - }); + const coreNavLinks = this.chrome.navLinks; + /** + * Modify last url for deleted saved objects to avoid loading pages with "Could not locate..." + */ + coreNavLinks + .getAll() + .filter( + link => + link.linkToLastSubUrl && + idsUsed.find(deletedId => link.url && link.url.includes(deletedId)) !== undefined + ) + .forEach(link => coreNavLinks.update(link.id, { url: link.baseUrl })); } /** @@ -80,7 +97,7 @@ export class SavedObjectLoader { * @param id * @returns {source} The modified source object, with an id and url field. */ - mapHitSource(source, id) { + mapHitSource(source: Record, id: string) { source.id = id; source.url = this.urlFor(id); return source; @@ -92,7 +109,7 @@ export class SavedObjectLoader { * @param hit * @returns {hit.attributes} The modified hit.attributes object, with an id and url field. */ - mapSavedObjectApiHits(hit) { + mapSavedObjectApiHits(hit: { attributes: Record; id: string }) { return this.mapHitSource(hit.attributes, hit.id); } @@ -100,13 +117,14 @@ export class SavedObjectLoader { * TODO: Rather than use a hardcoded limit, implement pagination. See * https://github.com/elastic/kibana/issues/8044 for reference. * - * @param searchString + * @param search * @param size + * @param fields * @returns {Promise} */ - findAll(search = '', size = 100, fields) { - return this.savedObjectsClient.find( - { + findAll(search: string = '', size: number = 100, fields?: string[]) { + return this.savedObjectsClient + .find({ type: this.lowercaseType, search: search ? `${search}*` : undefined, perPage: size, @@ -114,20 +132,20 @@ export class SavedObjectLoader { searchFields: ['title^3', 'description'], defaultSearchOperator: 'AND', fields, - }).then((resp) => { - return { - total: resp.total, - hits: resp.savedObjects - .map((savedObject) => this.mapSavedObjectApiHits(savedObject)) - }; - }); + } as SavedObjectsFindOptions) + .then(resp => { + return { + total: resp.total, + hits: resp.savedObjects.map(savedObject => this.mapSavedObjectApiHits(savedObject)), + }; + }); } - find(search = '', size = 100) { + find(search: string = '', size: number = 100) { return this.findAll(search, size).then(resp => { return { total: resp.total, - hits: resp.hits.filter(savedObject => !savedObject.error) + hits: resp.hits.filter(savedObject => !savedObject.error), }; }); } diff --git a/src/legacy/ui/public/saved_objects/types.ts b/src/legacy/ui/public/saved_objects/types.ts new file mode 100644 index 000000000000..bccf73917882 --- /dev/null +++ b/src/legacy/ui/public/saved_objects/types.ts @@ -0,0 +1,90 @@ +/* + * 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 { ChromeStart, OverlayStart, SavedObjectsClientContract } from 'kibana/public'; +import { SearchSource, SearchSourceContract } from 'ui/courier'; +import { SavedObjectAttributes, SavedObjectReference } from 'kibana/server'; +import { IndexPatternsContract } from '../../../../plugins/data/public'; +import { IndexPattern } from '../../../core_plugins/data/public'; + +export interface SavedObject { + _serialize: () => { attributes: SavedObjectAttributes; references: SavedObjectReference[] }; + _source: Record; + applyESResp: (resp: EsResponse) => Promise; + copyOnSave: boolean; + creationOpts: (opts: SavedObjectCreationOpts) => Record; + defaults: any; + delete?: () => Promise<{}>; + destroy?: () => void; + getDisplayName: () => string; + getEsType: () => string; + getFullPath: () => string; + hydrateIndexPattern?: (id?: string) => Promise; + id?: string; + init?: () => Promise; + isSaving: boolean; + isTitleChanged: () => boolean; + lastSavedTitle: string; + migrationVersion?: Record; + save: (saveOptions: SavedObjectSaveOpts) => Promise; + searchSource?: SearchSourceContract; + showInRecentlyAccessed: boolean; + title: string; +} + +export interface SavedObjectSaveOpts { + confirmOverwrite?: boolean; + isTitleDuplicateConfirmed?: boolean; + onTitleDuplicate?: () => void; +} + +export interface SavedObjectCreationOpts { + references?: SavedObjectReference[]; + overwrite?: boolean; +} + +export interface SavedObjectKibanaServices { + savedObjectsClient: SavedObjectsClientContract; + indexPatterns: IndexPatternsContract; + chrome: ChromeStart; + overlays: OverlayStart; +} + +export interface SavedObjectConfig { + afterESResp?: () => any; + clearSavedIndexPattern?: boolean; + defaults?: any; + extractReferences?: (opts: { + attributes: SavedObjectAttributes; + references: SavedObjectReference[]; + }) => { + attributes: SavedObjectAttributes; + references: SavedObjectReference[]; + }; + id?: string; + init?: () => void; + indexPattern?: IndexPattern; + injectReferences?: any; + mapping?: any; + migrationVersion?: Record; + path?: string; + searchSource?: SearchSource | boolean; + type?: string; +} + +export type EsResponse = Record; diff --git a/x-pack/legacy/plugins/graph/public/types/persistence.ts b/x-pack/legacy/plugins/graph/public/types/persistence.ts index 9c59b7057fe6..7883e81fb9b8 100644 --- a/x-pack/legacy/plugins/graph/public/types/persistence.ts +++ b/x-pack/legacy/plugins/graph/public/types/persistence.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObject } from 'ui/saved_objects/saved_object'; +import { SavedObject } from 'ui/saved_objects/types'; import { AdvancedSettings, UrlTemplate, WorkspaceField } from './app_state'; import { WorkspaceNode, WorkspaceEdge } from './workspace_state'; diff --git a/x-pack/legacy/plugins/maps/public/angular/services/gis_map_saved_object_loader.js b/x-pack/legacy/plugins/maps/public/angular/services/gis_map_saved_object_loader.js index fdf5172fea8c..cff6fe878c9f 100644 --- a/x-pack/legacy/plugins/maps/public/angular/services/gis_map_saved_object_loader.js +++ b/x-pack/legacy/plugins/maps/public/angular/services/gis_map_saved_object_loader.js @@ -6,12 +6,12 @@ import './saved_gis_map'; import { uiModules } from 'ui/modules'; -import { SavedObjectLoader, SavedObjectsClientProvider } from 'ui/saved_objects'; +import { SavedObjectLoader } from 'ui/saved_objects'; +import { npStart } from '../../../../../../../src/legacy/ui/public/new_platform'; const module = uiModules.get('app/maps'); // This is the only thing that gets injected into controllers -module.service('gisMapSavedObjectLoader', function (Private, SavedGisMap, kbnUrl, chrome) { - const savedObjectClient = Private(SavedObjectsClientProvider); - return new SavedObjectLoader(SavedGisMap, kbnUrl, chrome, savedObjectClient); +module.service('gisMapSavedObjectLoader', function (SavedGisMap) { + return new SavedObjectLoader(SavedGisMap, npStart.core.savedObjects.client, npStart.core.chrome); });