mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
Deangularize and typescriptify ui/saved_object (#51562)
This commit is contained in:
parent
06eeb59da3
commit
85aea35c94
45 changed files with 1297 additions and 838 deletions
|
@ -41,5 +41,5 @@ export function getSavedDashboardMock(
|
|||
getQuery: () => ({ query: '', language: 'kuery' }),
|
||||
getFilters: () => [],
|
||||
...config,
|
||||
};
|
||||
} as SavedObjectDashboard;
|
||||
}
|
||||
|
|
|
@ -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<SaveResult> {
|
||||
function save(saveOptions: SavedObjectSaveOpts): Promise<SaveResult> {
|
||||
return saveDashboard(angular.toJson, timefilter, dashboardStateManager, saveOptions)
|
||||
.then(function(id) {
|
||||
if (id) {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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<string> {
|
||||
dashboardStateManager.saveState();
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -70,7 +70,7 @@ describe('discover field chooser directives', function () {
|
|||
on-remove-field="removeField"
|
||||
></disc-field-chooser>
|
||||
`);
|
||||
beforeEach(() => pluginInstance.initializeServices(true));
|
||||
beforeEach(() => pluginInstance.initializeServices());
|
||||
beforeEach(() => pluginInstance.initializeInnerAngular());
|
||||
|
||||
beforeEach(ngMock.module('app/discover', ($provide) => {
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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 () {
|
||||
|
|
|
@ -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 () {
|
||||
|
|
|
@ -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 () {
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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<SavedSearch>;
|
||||
getSavedSearchUrlById: (id: string) => Promise<string>;
|
||||
uiSettings: IUiSettingsClient;
|
||||
}
|
||||
|
||||
export async function buildGlobalAngularServices() {
|
||||
const injector = await chromeLegacy.dangerouslyGetActiveInjector();
|
||||
const Private = injector.get<IPrivate>('Private');
|
||||
const kbnUrl = injector.get<IPrivate>('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
|
||||
|
|
|
@ -106,11 +106,11 @@ export class DiscoverPlugin implements Plugin<DiscoverSetup, DiscoverStart> {
|
|||
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;
|
||||
};
|
||||
|
|
|
@ -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);
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
});
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 });
|
||||
};
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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<string>;
|
||||
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',
|
||||
}
|
||||
);
|
82
src/legacy/ui/public/saved_objects/helpers/apply_es_resp.ts
Normal file
82
src/legacy/ui/public/saved_objects/helpers/apply_es_resp.ts
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
126
src/legacy/ui/public/saved_objects/helpers/build_saved_object.ts
Normal file
126
src/legacy/ui/public/saved_objects/helpers/build_saved_object.ts
Normal file
|
@ -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<IndexPattern | null>}
|
||||
*/
|
||||
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<string, any> = {}) => ({
|
||||
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);
|
||||
};
|
||||
}
|
|
@ -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<true> 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<ErrorMsg>
|
||||
* @param savedObject
|
||||
* @param isTitleDuplicateConfirmed
|
||||
* @param onTitleDuplicate
|
||||
* @param services
|
||||
*/
|
||||
export async function checkForDuplicateTitle(
|
||||
savedObject: SavedObject,
|
||||
isTitleDuplicateConfirmed: boolean,
|
||||
onTitleDuplicate: (() => void) | undefined,
|
||||
services: SavedObjectKibanaServices
|
||||
): Promise<true> {
|
||||
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);
|
||||
}
|
|
@ -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<true> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const cancelButtonText = i18n.translate(
|
||||
'common.ui.savedObjects.confirmModal.cancelButtonLabel',
|
||||
{
|
||||
defaultMessage: 'Cancel',
|
||||
}
|
||||
);
|
||||
|
||||
const modal = overlays.openModal(
|
||||
toMountPoint(
|
||||
<EuiConfirmModal
|
||||
onCancel={() => {
|
||||
modal.close();
|
||||
reject();
|
||||
}}
|
||||
onConfirm={() => {
|
||||
modal.close();
|
||||
resolve(true);
|
||||
}}
|
||||
confirmButtonText={confirmBtnText}
|
||||
cancelButtonText={cancelButtonText}
|
||||
title={title}
|
||||
>
|
||||
{message}
|
||||
</EuiConfirmModal>
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
85
src/legacy/ui/public/saved_objects/helpers/create_source.ts
Normal file
85
src/legacy/ui/public/saved_objects/helpers/create_source.ts
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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<true> {
|
||||
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));
|
||||
}
|
||||
}
|
|
@ -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<SimpleSavedObject|undefined>}
|
||||
*/
|
||||
export function findObjectByTitle<T extends SavedObjectAttributes>(
|
||||
export async function findObjectByTitle<T extends SavedObjectAttributes>(
|
||||
savedObjectsClient: SavedObjectsClientContract,
|
||||
type: string,
|
||||
title: string
|
||||
): Promise<SimpleSavedObject<T> | 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<T>({
|
||||
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<T>({
|
||||
type,
|
||||
perPage: 10,
|
||||
search: `"${title}"`,
|
||||
searchFields: ['title'],
|
||||
fields: ['title'],
|
||||
});
|
||||
return response.savedObjects.find(obj => obj.get('title').toLowerCase() === title.toLowerCase());
|
||||
}
|
|
@ -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<IndexPattern | null>}
|
||||
*/
|
||||
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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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<string, any>;
|
||||
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<string, any>) => 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<string, any>, 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));
|
||||
}
|
||||
}
|
128
src/legacy/ui/public/saved_objects/helpers/save_saved_object.ts
Normal file
128
src/legacy/ui/public/saved_objects/helpers/save_saved_object.ts
Normal file
|
@ -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<string> {
|
||||
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);
|
||||
}
|
||||
}
|
|
@ -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<string, any>;
|
||||
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<string, any> = _.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 };
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -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<IndexPattern | null>}
|
||||
*/
|
||||
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;
|
||||
}
|
63
src/legacy/ui/public/saved_objects/saved_object.ts
Normal file
63
src/legacy/ui/public/saved_objects/saved_object.ts
Normal file
|
@ -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);
|
||||
}
|
|
@ -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<string, string>;
|
||||
|
||||
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<SavedObject>}
|
||||
*/
|
||||
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<string, unknown>, 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<string, unknown>; 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),
|
||||
};
|
||||
});
|
||||
}
|
90
src/legacy/ui/public/saved_objects/types.ts
Normal file
90
src/legacy/ui/public/saved_objects/types.ts
Normal file
|
@ -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<string, unknown>;
|
||||
applyESResp: (resp: EsResponse) => Promise<SavedObject>;
|
||||
copyOnSave: boolean;
|
||||
creationOpts: (opts: SavedObjectCreationOpts) => Record<string, unknown>;
|
||||
defaults: any;
|
||||
delete?: () => Promise<{}>;
|
||||
destroy?: () => void;
|
||||
getDisplayName: () => string;
|
||||
getEsType: () => string;
|
||||
getFullPath: () => string;
|
||||
hydrateIndexPattern?: (id?: string) => Promise<null | IndexPattern>;
|
||||
id?: string;
|
||||
init?: () => Promise<SavedObject>;
|
||||
isSaving: boolean;
|
||||
isTitleChanged: () => boolean;
|
||||
lastSavedTitle: string;
|
||||
migrationVersion?: Record<string, any>;
|
||||
save: (saveOptions: SavedObjectSaveOpts) => Promise<string>;
|
||||
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<string, any>;
|
||||
path?: string;
|
||||
searchSource?: SearchSource | boolean;
|
||||
type?: string;
|
||||
}
|
||||
|
||||
export type EsResponse = Record<string, any>;
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue