diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index f9f43b804fc92..f327206464090 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -131,6 +131,7 @@ /src/legacy/server/saved_objects/ @elastic/kibana-platform /src/legacy/server/status/ @elastic/kibana-platform /src/plugins/status_page/ @elastic/kibana-platform +/src/plugins/saved_objects_management/ @elastic/kibana-platform /src/dev/run_check_published_api_changes.ts @elastic/kibana-platform # Security diff --git a/src/core/server/saved_objects/index.ts b/src/core/server/saved_objects/index.ts index 0af8ea7d0e830..b50e47b9eab73 100644 --- a/src/core/server/saved_objects/index.ts +++ b/src/core/server/saved_objects/index.ts @@ -21,8 +21,6 @@ export * from './service'; export { SavedObjectsSchema } from './schema'; -export { SavedObjectsManagement } from './management'; - export * from './import'; export { diff --git a/src/core/server/saved_objects/service/index.ts b/src/core/server/saved_objects/service/index.ts index 9f625b4732e26..f44824238aa21 100644 --- a/src/core/server/saved_objects/service/index.ts +++ b/src/core/server/saved_objects/service/index.ts @@ -36,6 +36,7 @@ export interface SavedObjectsLegacyService { getScopedSavedObjectsClient: SavedObjectsClientProvider['getClient']; SavedObjectsClient: typeof SavedObjectsClient; types: string[]; + importAndExportableTypes: string[]; schema: SavedObjectsSchema; getSavedObjectsRepository(...rest: any[]): any; importExport: { diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 9317481ae2f8f..a0a4c9e07b2a5 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -2028,6 +2028,8 @@ export interface SavedObjectsLegacyService { // (undocumented) getScopedSavedObjectsClient: SavedObjectsClientProvider['getClient']; // (undocumented) + importAndExportableTypes: string[]; + // (undocumented) importExport: { objectLimit: number; importSavedObjects(options: SavedObjectsImportOptions): Promise; diff --git a/src/legacy/core_plugins/kibana/index.js b/src/legacy/core_plugins/kibana/index.js index 1d772536fa1ea..cac9ef30fba4f 100644 --- a/src/legacy/core_plugins/kibana/index.js +++ b/src/legacy/core_plugins/kibana/index.js @@ -24,7 +24,6 @@ import { promisify } from 'util'; import { migrations } from './migrations'; import { importApi } from './server/routes/api/import'; import { exportApi } from './server/routes/api/export'; -import { managementApi } from './server/routes/api/management'; import mappings from './mappings.json'; import { getUiSettingDefaults } from './ui_setting_defaults'; import { registerCspCollector } from './server/lib/csp_usage_collector'; @@ -259,7 +258,6 @@ export default function(kibana) { // routes importApi(server); exportApi(server); - managementApi(server); registerCspCollector(usageCollection, server); server.injectUiAppVars('kibana', () => injectVars(server)); }, diff --git a/src/legacy/core_plugins/kibana/inject_vars.js b/src/legacy/core_plugins/kibana/inject_vars.js index 01623341e4d38..76d1704907ab5 100644 --- a/src/legacy/core_plugins/kibana/inject_vars.js +++ b/src/legacy/core_plugins/kibana/inject_vars.js @@ -20,12 +20,7 @@ export function injectVars(server) { const serverConfig = server.config(); - // Get types that are import and exportable, by default yes unless isImportableAndExportable is set to false - const { types: allTypes } = server.savedObjects; - const savedObjectsManagement = server.getSavedObjectsManagement(); - const importAndExportableTypes = allTypes.filter(type => - savedObjectsManagement.isImportAndExportable(type) - ); + const { importAndExportableTypes } = server.savedObjects; return { importAndExportableTypes, diff --git a/src/legacy/core_plugins/kibana/server/routes/api/management/saved_objects/find.js b/src/legacy/core_plugins/kibana/server/routes/api/management/saved_objects/find.js deleted file mode 100644 index 920b5c43678d1..0000000000000 --- a/src/legacy/core_plugins/kibana/server/routes/api/management/saved_objects/find.js +++ /dev/null @@ -1,107 +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. - */ - -/** - * This file wraps the saved object `_find` API and is designed specifically for the saved object - * management UI. The main difference is this will inject a root `meta` attribute on each saved object - * that the UI depends on. The meta fields come from functions within uiExports which can't be - * injected into the front end when defined within uiExports. There are alternatives to this but have - * decided to go with this approach at the time of development. - */ - -import Joi from 'joi'; -import { injectMetaAttributes } from '../../../../lib/management/saved_objects/inject_meta_attributes'; - -export function registerFind(server) { - server.route({ - path: '/api/kibana/management/saved_objects/_find', - method: 'GET', - config: { - validate: { - query: Joi.object() - .keys({ - perPage: Joi.number() - .min(0) - .default(20), - page: Joi.number() - .min(0) - .default(1), - type: Joi.array() - .items(Joi.string()) - .single() - .required(), - search: Joi.string() - .allow('') - .optional(), - defaultSearchOperator: Joi.string() - .valid('OR', 'AND') - .default('OR'), - sortField: Joi.string(), - hasReference: Joi.object() - .keys({ - type: Joi.string().required(), - id: Joi.string().required(), - }) - .optional(), - fields: Joi.array() - .items(Joi.string()) - .single(), - }) - .default(), - }, - }, - async handler(request) { - const searchFields = new Set(); - const searchTypes = request.query.type; - const savedObjectsClient = request.getSavedObjectsClient(); - const savedObjectsManagement = server.getSavedObjectsManagement(); - const importAndExportableTypes = searchTypes.filter(type => - savedObjectsManagement.isImportAndExportable(type) - ); - - // Accumulate "defaultSearchField" attributes from savedObjectsManagement. Unfortunately - // search fields apply to all types of saved objects, the sum of these fields will - // be searched on for each object. - for (const type of importAndExportableTypes) { - const searchField = savedObjectsManagement.getDefaultSearchField(type); - if (searchField) { - searchFields.add(searchField); - } - } - - const findResponse = await savedObjectsClient.find({ - ...request.query, - fields: undefined, - searchFields: [...searchFields], - }); - return { - ...findResponse, - saved_objects: findResponse.saved_objects - .map(obj => injectMetaAttributes(obj, savedObjectsManagement)) - .map(obj => { - const result = { ...obj, attributes: {} }; - for (const field of request.query.fields || []) { - result.attributes[field] = obj.attributes[field]; - } - return result; - }), - }; - }, - }); -} diff --git a/src/legacy/core_plugins/kibana/server/routes/api/management/saved_objects/relationships.js b/src/legacy/core_plugins/kibana/server/routes/api/management/saved_objects/relationships.js deleted file mode 100644 index eb6a7fc7b5195..0000000000000 --- a/src/legacy/core_plugins/kibana/server/routes/api/management/saved_objects/relationships.js +++ /dev/null @@ -1,59 +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 Joi from 'joi'; -import { findRelationships } from '../../../../lib/management/saved_objects/relationships'; - -export function registerRelationships(server) { - server.route({ - path: '/api/kibana/management/saved_objects/relationships/{type}/{id}', - method: ['GET'], - config: { - validate: { - params: Joi.object().keys({ - type: Joi.string(), - id: Joi.string(), - }), - query: Joi.object().keys({ - size: Joi.number().default(10000), - savedObjectTypes: Joi.array() - .single() - .items(Joi.string()) - .required(), - }), - }, - }, - - handler: async req => { - const type = req.params.type; - const id = req.params.id; - const size = req.query.size; - const savedObjectTypes = req.query.savedObjectTypes; - const savedObjectsClient = req.getSavedObjectsClient(); - const savedObjectsManagement = req.server.getSavedObjectsManagement(); - - return await findRelationships(type, id, { - size, - savedObjectsClient, - savedObjectsManagement, - savedObjectTypes, - }); - }, - }); -} diff --git a/src/legacy/core_plugins/kibana/server/routes/api/management/saved_objects/scroll.js b/src/legacy/core_plugins/kibana/server/routes/api/management/saved_objects/scroll.js deleted file mode 100644 index b3edd42149d45..0000000000000 --- a/src/legacy/core_plugins/kibana/server/routes/api/management/saved_objects/scroll.js +++ /dev/null @@ -1,120 +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 Joi from 'joi'; - -async function findAll(savedObjectsClient, findOptions, page = 1, allObjects = []) { - const objects = await savedObjectsClient.find({ - ...findOptions, - page, - }); - - allObjects.push(...objects.saved_objects); - if (allObjects.length < objects.total) { - return findAll(savedObjectsClient, findOptions, page + 1, allObjects); - } - - return allObjects; -} - -export function registerScrollForExportRoute(server) { - server.route({ - path: '/api/kibana/management/saved_objects/scroll/export', - method: ['POST'], - config: { - validate: { - payload: Joi.object() - .keys({ - typesToInclude: Joi.array() - .items(Joi.string()) - .required(), - }) - .required(), - }, - }, - - handler: async req => { - const savedObjectsClient = req.getSavedObjectsClient(); - const objects = await findAll(savedObjectsClient, { - perPage: 1000, - type: req.payload.typesToInclude, - }); - - return objects.map(hit => { - return { - _id: hit.id, - _source: hit.attributes, - _meta: { - savedObjectVersion: 2, - }, - _migrationVersion: hit.migrationVersion, - _references: hit.references || [], - }; - }); - }, - }); -} - -export function registerScrollForCountRoute(server) { - server.route({ - path: '/api/kibana/management/saved_objects/scroll/counts', - method: ['POST'], - config: { - validate: { - payload: Joi.object() - .keys({ - typesToInclude: Joi.array() - .items(Joi.string()) - .required(), - searchString: Joi.string(), - }) - .required(), - }, - }, - - handler: async req => { - const savedObjectsClient = req.getSavedObjectsClient(); - const findOptions = { - type: req.payload.typesToInclude, - perPage: 1000, - }; - - if (req.payload.searchString) { - findOptions.search = `${req.payload.searchString}*`; - findOptions.searchFields = ['title']; - } - - const objects = await findAll(savedObjectsClient, findOptions); - const counts = objects.reduce((accum, result) => { - const type = result.type; - accum[type] = accum[type] || 0; - accum[type]++; - return accum; - }, {}); - - for (const type of req.payload.typesToInclude) { - if (!counts[type]) { - counts[type] = 0; - } - } - - return counts; - }, - }); -} diff --git a/src/legacy/core_plugins/kibana/server/routes/api/management/saved_objects/scroll.test.js b/src/legacy/core_plugins/kibana/server/routes/api/management/saved_objects/scroll.test.js deleted file mode 100644 index 0d14da39d73b3..0000000000000 --- a/src/legacy/core_plugins/kibana/server/routes/api/management/saved_objects/scroll.test.js +++ /dev/null @@ -1,95 +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 Hapi from 'hapi'; -import { registerScrollForExportRoute } from './scroll'; - -const createMockServer = () => { - const mockServer = new Hapi.Server({ - debug: false, - port: 8080, - routes: { - validate: { - failAction: (r, h, err) => { - throw err; - }, - }, - }, - }); - return mockServer; -}; - -describe(`POST /api/kibana/management/saved_objects/scroll/export`, () => { - test('requires "typesToInclude"', async () => { - const mockServer = createMockServer(); - registerScrollForExportRoute(mockServer); - - const headers = {}; - const payload = {}; - - const request = { - method: 'POST', - url: `/api/kibana/management/saved_objects/scroll/export`, - headers, - payload, - }; - - const { result, statusCode } = await mockServer.inject(request); - expect(statusCode).toEqual(400); - expect(result).toMatchObject({ - message: `child "typesToInclude" fails because ["typesToInclude" is required]`, - }); - }); - - test(`uses "typesToInclude" when searching for objects to export`, async () => { - const mockServer = createMockServer(); - const mockClient = { - find: jest.fn(() => { - return { - saved_objects: [], - }; - }), - }; - - mockServer.decorate('request', 'getSavedObjectsClient', () => mockClient); - - registerScrollForExportRoute(mockServer); - - const headers = {}; - const payload = { - typesToInclude: ['foo', 'bar'], - }; - - const request = { - method: 'POST', - url: `/api/kibana/management/saved_objects/scroll/export`, - headers, - payload, - }; - - const { result, statusCode } = await mockServer.inject(request); - expect(statusCode).toEqual(200); - expect(result).toEqual([]); - - expect(mockClient.find).toHaveBeenCalledWith({ - page: 1, - perPage: 1000, - type: ['foo', 'bar'], - }); - }); -}); diff --git a/src/legacy/server/kbn_server.d.ts b/src/legacy/server/kbn_server.d.ts index d222dbb550f11..d43ddf581da90 100644 --- a/src/legacy/server/kbn_server.d.ts +++ b/src/legacy/server/kbn_server.d.ts @@ -39,9 +39,6 @@ import { LegacyServiceDiscoverPlugins, } from '../../core/server'; -// Disable lint errors for imports from src/core/server/saved_objects until SavedObjects migration is complete -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { SavedObjectsManagement } from '../../core/server/saved_objects/management'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { LegacyConfig, ILegacyService, ILegacyInternals } from '../../core/server/legacy'; import { ApmOssPlugin } from '../core_plugins/apm_oss'; @@ -78,7 +75,6 @@ declare module 'hapi' { addScopedTutorialContextFactory: ( scopedTutorialContextFactory: (...args: any[]) => any ) => void; - getSavedObjectsManagement(): SavedObjectsManagement; getInjectedUiAppVars: (pluginName: string) => { [key: string]: any }; getUiNavLinks(): Array<{ _id: string }>; addMemoizedFactoryToRequest: ( diff --git a/src/legacy/server/saved_objects/saved_objects_mixin.js b/src/legacy/server/saved_objects/saved_objects_mixin.js index cc63099c8a211..bcf766231dc9c 100644 --- a/src/legacy/server/saved_objects/saved_objects_mixin.js +++ b/src/legacy/server/saved_objects/saved_objects_mixin.js @@ -29,7 +29,6 @@ import { } from '../../../core/server/saved_objects'; import { getRootPropertiesObjects } from '../../../core/server/saved_objects/mappings'; import { convertTypesToLegacySchema } from '../../../core/server/saved_objects/utils'; -import { SavedObjectsManagement } from '../../../core/server/saved_objects/management'; export function savedObjectsMixin(kbnServer, server) { const migrator = kbnServer.newPlatform.__internals.kibanaMigrator; @@ -40,11 +39,6 @@ export function savedObjectsMixin(kbnServer, server) { const visibleTypes = allTypes.filter(type => !schema.isHiddenType(type)); server.decorate('server', 'kibanaMigrator', migrator); - server.decorate( - 'server', - 'getSavedObjectsManagement', - () => new SavedObjectsManagement(typeRegistry) - ); const warn = message => server.log(['warning', 'saved-objects'], message); // we use kibana.index which is technically defined in the kibana plugin, so if @@ -84,8 +78,13 @@ export function savedObjectsMixin(kbnServer, server) { const provider = kbnServer.newPlatform.__internals.savedObjectsClientProvider; + const importAndExportableTypes = typeRegistry + .getImportableAndExportableTypes() + .map(type => type.name); + const service = { types: visibleTypes, + importAndExportableTypes, SavedObjectsClient, SavedObjectsRepository, getSavedObjectsRepository: createRepository, diff --git a/src/legacy/server/saved_objects/saved_objects_mixin.test.js b/src/legacy/server/saved_objects/saved_objects_mixin.test.js index 3745f0b92123c..3fa9f9a936988 100644 --- a/src/legacy/server/saved_objects/saved_objects_mixin.test.js +++ b/src/legacy/server/saved_objects/saved_objects_mixin.test.js @@ -183,7 +183,7 @@ describe('Saved Objects Mixin', () => { 'kibanaMigrator', expect.any(Object) ); - expect(mockServer.decorate).toHaveBeenCalledTimes(2); + expect(mockServer.decorate).toHaveBeenCalledTimes(1); expect(mockServer.route).not.toHaveBeenCalled(); }); }); diff --git a/src/plugins/saved_objects_management/kibana.json b/src/plugins/saved_objects_management/kibana.json new file mode 100644 index 0000000000000..83c3034e17ef3 --- /dev/null +++ b/src/plugins/saved_objects_management/kibana.json @@ -0,0 +1,6 @@ +{ + "id": "savedObjectsManagement", + "version": "kibana", + "server": true, + "ui": false +} diff --git a/src/plugins/saved_objects_management/server/index.ts b/src/plugins/saved_objects_management/server/index.ts new file mode 100644 index 0000000000000..820118ebf3ec4 --- /dev/null +++ b/src/plugins/saved_objects_management/server/index.ts @@ -0,0 +1,31 @@ +/* + * 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 { PluginInitializerContext } from 'src/core/server'; +import { SavedObjectsManagementPlugin } from './plugin'; + +export const plugin = (context: PluginInitializerContext) => + new SavedObjectsManagementPlugin(context); + +export { + SavedObjectsManagementPluginSetup, + SavedObjectsManagementPluginStart, + SavedObjectMetadata, + SavedObjectWithMetadata, +} from './types'; diff --git a/src/plugins/saved_objects_management/server/lib/find_all.test.ts b/src/plugins/saved_objects_management/server/lib/find_all.test.ts new file mode 100644 index 0000000000000..98e669c093178 --- /dev/null +++ b/src/plugins/saved_objects_management/server/lib/find_all.test.ts @@ -0,0 +1,95 @@ +/* + * 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 { times } from 'lodash'; +import { SavedObjectsFindOptions, SavedObject } from 'src/core/server'; +import { savedObjectsClientMock } from '../../../../core/server/mocks'; +import { findAll } from './find_all'; + +describe('findAll', () => { + let savedObjectsClient: ReturnType; + + const createObj = (id: number): SavedObject => ({ + type: 'type', + id: `id-${id}`, + attributes: {}, + references: [], + }); + + beforeEach(() => { + savedObjectsClient = savedObjectsClientMock.create(); + }); + + it('calls the saved object client with the correct parameters', async () => { + const query: SavedObjectsFindOptions = { + type: ['some-type', 'another-type'], + }; + + savedObjectsClient.find.mockResolvedValue({ + saved_objects: [createObj(1), createObj(2)], + total: 1, + per_page: 20, + page: 1, + }); + + const results = await findAll(savedObjectsClient, query); + + expect(savedObjectsClient.find).toHaveBeenCalledTimes(1); + expect(savedObjectsClient.find).toHaveBeenCalledWith({ + ...query, + page: 1, + }); + + expect(results).toEqual([createObj(1), createObj(2)]); + }); + + it('recursively call find until all objects are fetched', async () => { + const query: SavedObjectsFindOptions = { + type: ['some-type', 'another-type'], + }; + const objPerPage = 2; + + savedObjectsClient.find.mockImplementation(({ page }) => { + const firstInPage = (page! - 1) * objPerPage + 1; + return Promise.resolve({ + saved_objects: [createObj(firstInPage), createObj(firstInPage + 1)], + total: objPerPage * 3, + per_page: objPerPage, + page: page!, + }); + }); + + const results = await findAll(savedObjectsClient, query); + expect(savedObjectsClient.find).toHaveBeenCalledTimes(3); + expect(savedObjectsClient.find).toHaveBeenCalledWith({ + ...query, + page: 1, + }); + expect(savedObjectsClient.find).toHaveBeenCalledWith({ + ...query, + page: 2, + }); + expect(savedObjectsClient.find).toHaveBeenCalledWith({ + ...query, + page: 3, + }); + + expect(results).toEqual(times(6, num => createObj(num + 1))); + }); +}); diff --git a/src/plugins/saved_objects_management/server/lib/find_all.ts b/src/plugins/saved_objects_management/server/lib/find_all.ts new file mode 100644 index 0000000000000..6bc3e46d026bf --- /dev/null +++ b/src/plugins/saved_objects_management/server/lib/find_all.ts @@ -0,0 +1,46 @@ +/* + * 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 { SavedObjectsClientContract, SavedObject, SavedObjectsFindOptions } from 'src/core/server'; + +export const findAll = async ( + client: SavedObjectsClientContract, + findOptions: SavedObjectsFindOptions +): Promise => { + return recursiveFind(client, findOptions, 1, []); +}; + +const recursiveFind = async ( + client: SavedObjectsClientContract, + findOptions: SavedObjectsFindOptions, + page: number, + allObjects: SavedObject[] +): Promise => { + const objects = await client.find({ + ...findOptions, + page, + }); + + allObjects.push(...objects.saved_objects); + if (allObjects.length < objects.total) { + return recursiveFind(client, findOptions, page + 1, allObjects); + } + + return allObjects; +}; diff --git a/src/plugins/saved_objects_management/server/lib/find_relationships.test.ts b/src/plugins/saved_objects_management/server/lib/find_relationships.test.ts new file mode 100644 index 0000000000000..18846eeebdfb7 --- /dev/null +++ b/src/plugins/saved_objects_management/server/lib/find_relationships.test.ts @@ -0,0 +1,213 @@ +/* + * 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 { findRelationships } from './find_relationships'; +import { managementMock } from '../services/management.mock'; +import { savedObjectsClientMock } from '../../../../core/server/mocks'; + +describe('findRelationships', () => { + let savedObjectsClient: ReturnType; + let managementService: ReturnType; + + beforeEach(() => { + savedObjectsClient = savedObjectsClientMock.create(); + managementService = managementMock.create(); + }); + + it('returns the child and parent references of the object', async () => { + const type = 'dashboard'; + const id = 'some-id'; + const references = [ + { + type: 'some-type', + id: 'ref-1', + name: 'ref 1', + }, + { + type: 'another-type', + id: 'ref-2', + name: 'ref 2', + }, + ]; + const referenceTypes = ['some-type', 'another-type']; + + savedObjectsClient.get.mockResolvedValue({ + id, + type, + attributes: {}, + references, + }); + + savedObjectsClient.bulkGet.mockResolvedValue({ + saved_objects: [ + { + type: 'some-type', + id: 'ref-1', + attributes: {}, + references: [], + }, + { + type: 'another-type', + id: 'ref-2', + attributes: {}, + references: [], + }, + ], + }); + + savedObjectsClient.find.mockResolvedValue({ + saved_objects: [ + { + type: 'parent-type', + id: 'parent-id', + attributes: {}, + references: [], + }, + ], + total: 1, + per_page: 20, + page: 1, + }); + + const relationships = await findRelationships({ + type, + id, + size: 20, + client: savedObjectsClient, + referenceTypes, + savedObjectsManagement: managementService, + }); + + expect(savedObjectsClient.get).toHaveBeenCalledTimes(1); + expect(savedObjectsClient.get).toHaveBeenCalledWith(type, id); + + expect(savedObjectsClient.bulkGet).toHaveBeenCalledTimes(1); + expect(savedObjectsClient.bulkGet).toHaveBeenCalledWith( + references.map(ref => ({ + id: ref.id, + type: ref.type, + })) + ); + + expect(savedObjectsClient.find).toHaveBeenCalledTimes(1); + expect(savedObjectsClient.find).toHaveBeenCalledWith({ + hasReference: { type, id }, + perPage: 20, + type: referenceTypes, + }); + + expect(relationships).toEqual([ + { + id: 'ref-1', + relationship: 'child', + type: 'some-type', + meta: expect.any(Object), + }, + { + id: 'ref-2', + relationship: 'child', + type: 'another-type', + meta: expect.any(Object), + }, + { + id: 'parent-id', + relationship: 'parent', + type: 'parent-type', + meta: expect.any(Object), + }, + ]); + }); + + it('uses the management service to consolidate the relationship objects', async () => { + const type = 'dashboard'; + const id = 'some-id'; + const references = [ + { + type: 'some-type', + id: 'ref-1', + name: 'ref 1', + }, + ]; + const referenceTypes = ['some-type', 'another-type']; + + managementService.getIcon.mockReturnValue('icon'); + managementService.getTitle.mockReturnValue('title'); + managementService.getEditUrl.mockReturnValue('editUrl'); + managementService.getInAppUrl.mockReturnValue({ + path: 'path', + uiCapabilitiesPath: 'uiCapabilitiesPath', + }); + + savedObjectsClient.get.mockResolvedValue({ + id, + type, + attributes: {}, + references, + }); + + savedObjectsClient.bulkGet.mockResolvedValue({ + saved_objects: [ + { + type: 'some-type', + id: 'ref-1', + attributes: {}, + references: [], + }, + ], + }); + + savedObjectsClient.find.mockResolvedValue({ + saved_objects: [], + total: 0, + per_page: 20, + page: 1, + }); + + const relationships = await findRelationships({ + type, + id, + size: 20, + client: savedObjectsClient, + referenceTypes, + savedObjectsManagement: managementService, + }); + + expect(managementService.getIcon).toHaveBeenCalledTimes(1); + expect(managementService.getTitle).toHaveBeenCalledTimes(1); + expect(managementService.getEditUrl).toHaveBeenCalledTimes(1); + expect(managementService.getInAppUrl).toHaveBeenCalledTimes(1); + + expect(relationships).toEqual([ + { + id: 'ref-1', + relationship: 'child', + type: 'some-type', + meta: { + title: 'title', + icon: 'icon', + editUrl: 'editUrl', + inAppUrl: { + path: 'path', + uiCapabilitiesPath: 'uiCapabilitiesPath', + }, + }, + }, + ]); + }); +}); diff --git a/src/plugins/saved_objects_management/server/lib/find_relationships.ts b/src/plugins/saved_objects_management/server/lib/find_relationships.ts new file mode 100644 index 0000000000000..cca8831afd16c --- /dev/null +++ b/src/plugins/saved_objects_management/server/lib/find_relationships.ts @@ -0,0 +1,88 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { SavedObjectsClientContract } from 'src/core/server'; +import { injectMetaAttributes } from './inject_meta_attributes'; +import { ISavedObjectsManagement } from '../services'; +import { SavedObjectRelation, SavedObjectWithMetadata } from '../types'; + +export async function findRelationships({ + type, + id, + size, + client, + referenceTypes, + savedObjectsManagement, +}: { + type: string; + id: string; + size: number; + client: SavedObjectsClientContract; + referenceTypes: string[]; + savedObjectsManagement: ISavedObjectsManagement; +}): Promise { + const { references = [] } = await client.get(type, id); + + // Use a map to avoid duplicates, it does happen but have a different "name" in the reference + const referencedToBulkGetOpts = new Map( + references.map(ref => [`${ref.type}:${ref.id}`, { id: ref.id, type: ref.type }]) + ); + + const [childReferencesResponse, parentReferencesResponse] = await Promise.all([ + referencedToBulkGetOpts.size > 0 + ? client.bulkGet([...referencedToBulkGetOpts.values()]) + : Promise.resolve({ saved_objects: [] }), + client.find({ + hasReference: { type, id }, + perPage: size, + type: referenceTypes, + }), + ]); + + return childReferencesResponse.saved_objects + .map(obj => injectMetaAttributes(obj, savedObjectsManagement)) + .map(extractCommonProperties) + .map( + obj => + ({ + ...obj, + relationship: 'child', + } as SavedObjectRelation) + ) + .concat( + parentReferencesResponse.saved_objects + .map(obj => injectMetaAttributes(obj, savedObjectsManagement)) + .map(extractCommonProperties) + .map( + obj => + ({ + ...obj, + relationship: 'parent', + } as SavedObjectRelation) + ) + ); +} + +function extractCommonProperties(savedObject: SavedObjectWithMetadata) { + return { + id: savedObject.id, + type: savedObject.type, + meta: savedObject.meta, + }; +} diff --git a/test/api_integration/apis/management/saved_objects/index.js b/src/plugins/saved_objects_management/server/lib/index.ts similarity index 81% rename from test/api_integration/apis/management/saved_objects/index.js rename to src/plugins/saved_objects_management/server/lib/index.ts index 01fab4a3c74b1..dea6813d690ec 100644 --- a/test/api_integration/apis/management/saved_objects/index.js +++ b/src/plugins/saved_objects_management/server/lib/index.ts @@ -17,9 +17,6 @@ * under the License. */ -export default function({ loadTestFile }) { - describe('saved_objects', () => { - loadTestFile(require.resolve('./find')); - loadTestFile(require.resolve('./relationships')); - }); -} +export { injectMetaAttributes } from './inject_meta_attributes'; +export { findAll } from './find_all'; +export { findRelationships } from './find_relationships'; diff --git a/src/plugins/saved_objects_management/server/lib/inject_meta_attributes.test.ts b/src/plugins/saved_objects_management/server/lib/inject_meta_attributes.test.ts new file mode 100644 index 0000000000000..0c0f9d8feb506 --- /dev/null +++ b/src/plugins/saved_objects_management/server/lib/inject_meta_attributes.test.ts @@ -0,0 +1,82 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { SavedObject } from 'src/core/server'; +import { injectMetaAttributes } from './inject_meta_attributes'; +import { managementMock } from '../services/management.mock'; + +describe('injectMetaAttributes', () => { + let managementService: ReturnType; + + beforeEach(() => { + managementService = managementMock.create(); + + managementService.getIcon.mockReturnValue('icon'); + managementService.getTitle.mockReturnValue('title'); + managementService.getEditUrl.mockReturnValue('editUrl'); + managementService.getInAppUrl.mockReturnValue({ + path: 'path', + uiCapabilitiesPath: 'uiCapabilitiesPath', + }); + }); + + it('inject the metadata to the obj', () => { + const obj: SavedObject = { + id: 'id', + type: 'config', + attributes: { some: 'value' }, + references: [], + }; + + const objWithMeta = injectMetaAttributes(obj, managementService); + expect(objWithMeta).toStrictEqual({ + id: 'id', + type: 'config', + attributes: { some: 'value' }, + references: [], + meta: { + icon: 'icon', + title: 'title', + editUrl: 'editUrl', + inAppUrl: { + path: 'path', + uiCapabilitiesPath: 'uiCapabilitiesPath', + }, + }, + }); + }); + + it('does not alter the original object', () => { + const obj: SavedObject = { + id: 'id', + type: 'config', + attributes: { some: 'value' }, + references: [], + }; + + injectMetaAttributes(obj, managementService); + + expect(obj).toStrictEqual({ + id: 'id', + type: 'config', + attributes: { some: 'value' }, + references: [], + }); + }); +}); diff --git a/src/plugins/saved_objects_management/server/lib/inject_meta_attributes.ts b/src/plugins/saved_objects_management/server/lib/inject_meta_attributes.ts new file mode 100644 index 0000000000000..615caffd3b60b --- /dev/null +++ b/src/plugins/saved_objects_management/server/lib/inject_meta_attributes.ts @@ -0,0 +1,40 @@ +/* + * 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 } from 'src/core/server'; +import { ISavedObjectsManagement } from '../services'; +import { SavedObjectWithMetadata } from '../types'; + +export function injectMetaAttributes( + savedObject: SavedObject | SavedObjectWithMetadata, + savedObjectsManagement: ISavedObjectsManagement +): SavedObjectWithMetadata { + const result = { + ...savedObject, + meta: (savedObject as SavedObjectWithMetadata).meta || {}, + }; + + // Add extra meta information + result.meta.icon = savedObjectsManagement.getIcon(savedObject.type); + result.meta.title = savedObjectsManagement.getTitle(savedObject); + result.meta.editUrl = savedObjectsManagement.getEditUrl(savedObject); + result.meta.inAppUrl = savedObjectsManagement.getInAppUrl(savedObject); + + return result; +} diff --git a/test/api_integration/apis/management/index.js b/src/plugins/saved_objects_management/server/plugin.test.mocks.ts similarity index 84% rename from test/api_integration/apis/management/index.js rename to src/plugins/saved_objects_management/server/plugin.test.mocks.ts index a442e3e4fee74..95dba4563a528 100644 --- a/test/api_integration/apis/management/index.js +++ b/src/plugins/saved_objects_management/server/plugin.test.mocks.ts @@ -17,8 +17,8 @@ * under the License. */ -export default function({ loadTestFile }) { - describe('management apis', () => { - loadTestFile(require.resolve('./saved_objects')); - }); -} +export const registerRoutesMock = jest.fn(); + +jest.doMock('./routes', () => ({ + registerRoutes: registerRoutesMock, +})); diff --git a/src/plugins/saved_objects_management/server/plugin.test.ts b/src/plugins/saved_objects_management/server/plugin.test.ts new file mode 100644 index 0000000000000..6c2c12c10efca --- /dev/null +++ b/src/plugins/saved_objects_management/server/plugin.test.ts @@ -0,0 +1,45 @@ +/* + * 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 { registerRoutesMock } from './plugin.test.mocks'; +import { SavedObjectsManagementPlugin } from './plugin'; +import { coreMock } from '../../../core/server/mocks'; + +describe('SavedObjectsManagementPlugin', () => { + let plugin: SavedObjectsManagementPlugin; + + beforeEach(() => { + plugin = new SavedObjectsManagementPlugin(coreMock.createPluginInitializerContext()); + }); + + describe('#setup', () => { + it('registers the routes', async () => { + const coreSetup = coreMock.createSetup(); + + await plugin.setup(coreSetup); + + expect(registerRoutesMock).toHaveBeenCalledTimes(1); + expect(registerRoutesMock).toHaveBeenCalledWith( + expect.objectContaining({ + http: coreSetup.http, + }) + ); + }); + }); +}); diff --git a/src/plugins/saved_objects_management/server/plugin.ts b/src/plugins/saved_objects_management/server/plugin.ts new file mode 100644 index 0000000000000..b72644b500967 --- /dev/null +++ b/src/plugins/saved_objects_management/server/plugin.ts @@ -0,0 +1,53 @@ +/* + * 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 { Subject } from 'rxjs'; +import { first } from 'rxjs/operators'; +import { CoreSetup, CoreStart, Logger, Plugin, PluginInitializerContext } from 'src/core/server'; +import { SavedObjectsManagementPluginSetup, SavedObjectsManagementPluginStart } from './types'; +import { SavedObjectsManagement } from './services'; +import { registerRoutes } from './routes'; + +export class SavedObjectsManagementPlugin + implements Plugin { + private readonly logger: Logger; + private managementService$ = new Subject(); + + constructor(private readonly context: PluginInitializerContext) { + this.logger = this.context.logger.get(); + } + + public async setup({ http }: CoreSetup) { + this.logger.debug('Setting up SavedObjectsManagement plugin'); + registerRoutes({ + http, + managementServicePromise: this.managementService$.pipe(first()).toPromise(), + }); + + return {}; + } + + public async start(core: CoreStart) { + this.logger.debug('Starting up SavedObjectsManagement plugin'); + const managementService = new SavedObjectsManagement(core.savedObjects.getTypeRegistry()); + this.managementService$.next(managementService); + + return {}; + } +} diff --git a/src/plugins/saved_objects_management/server/routes/find.ts b/src/plugins/saved_objects_management/server/routes/find.ts new file mode 100644 index 0000000000000..a74c92ba6161f --- /dev/null +++ b/src/plugins/saved_objects_management/server/routes/find.ts @@ -0,0 +1,97 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { schema } from '@kbn/config-schema'; +import { IRouter } from 'src/core/server'; +import { injectMetaAttributes } from '../lib'; +import { ISavedObjectsManagement } from '../services'; + +export const registerFindRoute = ( + router: IRouter, + managementServicePromise: Promise +) => { + router.get( + { + path: '/api/kibana/management/saved_objects/_find', + validate: { + query: schema.object({ + perPage: schema.number({ min: 0, defaultValue: 20 }), + page: schema.number({ min: 0, defaultValue: 1 }), + type: schema.oneOf([schema.string(), schema.arrayOf(schema.string())]), + search: schema.maybe(schema.string()), + defaultSearchOperator: schema.oneOf([schema.literal('OR'), schema.literal('AND')], { + defaultValue: 'OR', + }), + sortField: schema.maybe(schema.string()), + hasReference: schema.maybe( + schema.object({ + type: schema.string(), + id: schema.string(), + }) + ), + fields: schema.oneOf([schema.string(), schema.arrayOf(schema.string())], { + defaultValue: [], + }), + }), + }, + }, + router.handleLegacyErrors(async (context, req, res) => { + const managementService = await managementServicePromise; + const { client } = context.core.savedObjects; + const searchTypes = Array.isArray(req.query.type) ? req.query.type : [req.query.type]; + const includedFields = Array.isArray(req.query.fields) + ? req.query.fields + : [req.query.fields]; + const importAndExportableTypes = searchTypes.filter(type => + managementService.isImportAndExportable(type) + ); + + const searchFields = new Set(); + importAndExportableTypes.forEach(type => { + const searchField = managementService.getDefaultSearchField(type); + if (searchField) { + searchFields.add(searchField); + } + }); + + const findResponse = await client.find({ + ...req.query, + fields: undefined, + searchFields: [...searchFields], + }); + + const enhancedSavedObjects = findResponse.saved_objects + .map(so => injectMetaAttributes(so, managementService)) + .map(obj => { + const result = { ...obj, attributes: {} as Record }; + for (const field of includedFields) { + result.attributes[field] = obj.attributes[field]; + } + return result; + }); + + return res.ok({ + body: { + ...findResponse, + saved_objects: enhancedSavedObjects, + }, + }); + }) + ); +}; diff --git a/src/plugins/saved_objects_management/server/routes/index.test.ts b/src/plugins/saved_objects_management/server/routes/index.test.ts new file mode 100644 index 0000000000000..f183972953dce --- /dev/null +++ b/src/plugins/saved_objects_management/server/routes/index.test.ts @@ -0,0 +1,65 @@ +/* + * 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 { registerRoutes } from './index'; +import { ISavedObjectsManagement } from '../services'; +import { coreMock, httpServiceMock } from '../../../../core/server/mocks'; + +describe('registerRoutes', () => { + it('registers the management routes', () => { + const router = httpServiceMock.createRouter(); + const httpSetup = coreMock.createSetup().http; + httpSetup.createRouter.mockReturnValue(router); + const managementPromise = Promise.resolve({} as ISavedObjectsManagement); + + registerRoutes({ + http: httpSetup, + managementServicePromise: managementPromise, + }); + + expect(httpSetup.createRouter).toHaveBeenCalledTimes(1); + expect(router.get).toHaveBeenCalledTimes(2); + expect(router.post).toHaveBeenCalledTimes(2); + + expect(router.get).toHaveBeenCalledWith( + expect.objectContaining({ + path: '/api/kibana/management/saved_objects/_find', + }), + expect.any(Function) + ); + expect(router.get).toHaveBeenCalledWith( + expect.objectContaining({ + path: '/api/kibana/management/saved_objects/relationships/{type}/{id}', + }), + expect.any(Function) + ); + expect(router.post).toHaveBeenCalledWith( + expect.objectContaining({ + path: '/api/kibana/management/saved_objects/scroll/counts', + }), + expect.any(Function) + ); + expect(router.post).toHaveBeenCalledWith( + expect.objectContaining({ + path: '/api/kibana/management/saved_objects/scroll/export', + }), + expect.any(Function) + ); + }); +}); diff --git a/src/plugins/saved_objects_management/server/routes/index.ts b/src/plugins/saved_objects_management/server/routes/index.ts new file mode 100644 index 0000000000000..2c6adb71ed3ce --- /dev/null +++ b/src/plugins/saved_objects_management/server/routes/index.ts @@ -0,0 +1,38 @@ +/* + * 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 { HttpServiceSetup } from 'src/core/server'; +import { ISavedObjectsManagement } from '../services'; +import { registerFindRoute } from './find'; +import { registerScrollForCountRoute } from './scroll_count'; +import { registerScrollForExportRoute } from './scroll_export'; +import { registerRelationshipsRoute } from './relationships'; + +interface RegisterRouteOptions { + http: HttpServiceSetup; + managementServicePromise: Promise; +} + +export function registerRoutes({ http, managementServicePromise }: RegisterRouteOptions) { + const router = http.createRouter(); + registerFindRoute(router, managementServicePromise); + registerScrollForCountRoute(router); + registerScrollForExportRoute(router); + registerRelationshipsRoute(router, managementServicePromise); +} diff --git a/src/plugins/saved_objects_management/server/routes/relationships.ts b/src/plugins/saved_objects_management/server/routes/relationships.ts new file mode 100644 index 0000000000000..c9001deb91237 --- /dev/null +++ b/src/plugins/saved_objects_management/server/routes/relationships.ts @@ -0,0 +1,66 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { schema } from '@kbn/config-schema'; +import { IRouter } from 'src/core/server'; +import { findRelationships } from '../lib'; +import { ISavedObjectsManagement } from '../services'; + +export const registerRelationshipsRoute = ( + router: IRouter, + managementServicePromise: Promise +) => { + router.get( + { + path: '/api/kibana/management/saved_objects/relationships/{type}/{id}', + validate: { + params: schema.object({ + type: schema.string(), + id: schema.string(), + }), + query: schema.object({ + size: schema.number({ defaultValue: 10000 }), + savedObjectTypes: schema.oneOf([schema.string(), schema.arrayOf(schema.string())]), + }), + }, + }, + router.handleLegacyErrors(async (context, req, res) => { + const managementService = await managementServicePromise; + const { client } = context.core.savedObjects; + const { type, id } = req.params; + const { size } = req.query; + const savedObjectTypes = Array.isArray(req.query.savedObjectTypes) + ? req.query.savedObjectTypes + : [req.query.savedObjectTypes]; + + const relations = await findRelationships({ + type, + id, + client, + size, + referenceTypes: savedObjectTypes, + savedObjectsManagement: managementService, + }); + + return res.ok({ + body: relations, + }); + }) + ); +}; diff --git a/src/plugins/saved_objects_management/server/routes/scroll_count.ts b/src/plugins/saved_objects_management/server/routes/scroll_count.ts new file mode 100644 index 0000000000000..58ba90d847791 --- /dev/null +++ b/src/plugins/saved_objects_management/server/routes/scroll_count.ts @@ -0,0 +1,67 @@ +/* + * 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 { schema } from '@kbn/config-schema'; +import { IRouter, SavedObjectsFindOptions } from 'src/core/server'; +import { findAll } from '../lib'; + +export const registerScrollForCountRoute = (router: IRouter) => { + router.post( + { + path: '/api/kibana/management/saved_objects/scroll/counts', + validate: { + body: schema.object({ + typesToInclude: schema.arrayOf(schema.string()), + searchString: schema.maybe(schema.string()), + }), + }, + }, + router.handleLegacyErrors(async (context, req, res) => { + const { client } = context.core.savedObjects; + + const findOptions: SavedObjectsFindOptions = { + type: req.body.typesToInclude, + perPage: 1000, + }; + if (req.body.searchString) { + findOptions.search = `${req.body.searchString}*`; + findOptions.searchFields = ['title']; + } + + const objects = await findAll(client, findOptions); + + const counts = objects.reduce((accum, result) => { + const type = result.type; + accum[type] = accum[type] || 0; + accum[type]++; + return accum; + }, {} as Record); + + for (const type of req.body.typesToInclude) { + if (!counts[type]) { + counts[type] = 0; + } + } + + return res.ok({ + body: counts, + }); + }) + ); +}; diff --git a/src/plugins/saved_objects_management/server/routes/scroll_export.ts b/src/plugins/saved_objects_management/server/routes/scroll_export.ts new file mode 100644 index 0000000000000..cda2770234911 --- /dev/null +++ b/src/plugins/saved_objects_management/server/routes/scroll_export.ts @@ -0,0 +1,56 @@ +/* + * 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 { schema } from '@kbn/config-schema'; +import { IRouter } from 'src/core/server'; +import { findAll } from '../lib'; + +export const registerScrollForExportRoute = (router: IRouter) => { + router.post( + { + path: '/api/kibana/management/saved_objects/scroll/export', + validate: { + body: schema.object({ + typesToInclude: schema.arrayOf(schema.string()), + }), + }, + }, + router.handleLegacyErrors(async (context, req, res) => { + const { client } = context.core.savedObjects; + const objects = await findAll(client, { + perPage: 1000, + type: req.body.typesToInclude, + }); + + return res.ok({ + body: objects.map(hit => { + return { + _id: hit.id, + _source: hit.attributes, + _meta: { + savedObjectVersion: 2, + }, + _migrationVersion: hit.migrationVersion, + _references: hit.references || [], + }; + }), + }); + }) + ); +}; diff --git a/src/core/server/saved_objects/management/index.ts b/src/plugins/saved_objects_management/server/services/index.ts similarity index 90% rename from src/core/server/saved_objects/management/index.ts rename to src/plugins/saved_objects_management/server/services/index.ts index a256a1333c5cc..fddd53c73634e 100644 --- a/src/core/server/saved_objects/management/index.ts +++ b/src/plugins/saved_objects_management/server/services/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export { SavedObjectsManagement } from './management'; +export { SavedObjectsManagement, ISavedObjectsManagement } from './management'; diff --git a/src/core/server/saved_objects/management/management.mock.ts b/src/plugins/saved_objects_management/server/services/management.mock.ts similarity index 96% rename from src/core/server/saved_objects/management/management.mock.ts rename to src/plugins/saved_objects_management/server/services/management.mock.ts index e7242c30d3961..2099cc0f77bcc 100644 --- a/src/core/server/saved_objects/management/management.mock.ts +++ b/src/plugins/saved_objects_management/server/services/management.mock.ts @@ -24,7 +24,6 @@ const createManagementMock = () => { const mocked: jest.Mocked = { isImportAndExportable: jest.fn().mockReturnValue(true), getDefaultSearchField: jest.fn(), - getImportableAndExportableTypes: jest.fn(), getIcon: jest.fn(), getTitle: jest.fn(), getEditUrl: jest.fn(), diff --git a/src/core/server/saved_objects/management/management.test.ts b/src/plugins/saved_objects_management/server/services/management.test.ts similarity index 97% rename from src/core/server/saved_objects/management/management.test.ts rename to src/plugins/saved_objects_management/server/services/management.test.ts index dc110dec020f0..6b95048749fae 100644 --- a/src/core/server/saved_objects/management/management.test.ts +++ b/src/plugins/saved_objects_management/server/services/management.test.ts @@ -18,8 +18,7 @@ */ import { SavedObjectsManagement } from './management'; -import { SavedObjectsType } from '../types'; -import { SavedObjectTypeRegistry } from '../saved_objects_type_registry'; +import { SavedObjectsType, SavedObjectTypeRegistry } from '../../../../core/server'; describe('SavedObjectsManagement', () => { let registry: SavedObjectTypeRegistry; diff --git a/src/core/server/saved_objects/management/management.ts b/src/plugins/saved_objects_management/server/services/management.ts similarity index 86% rename from src/core/server/saved_objects/management/management.ts rename to src/plugins/saved_objects_management/server/services/management.ts index db759c4aec752..7aee974182497 100644 --- a/src/core/server/saved_objects/management/management.ts +++ b/src/plugins/saved_objects_management/server/services/management.ts @@ -17,19 +17,13 @@ * under the License. */ -import { SavedObject } from '../types'; -import { ISavedObjectTypeRegistry } from '../saved_objects_type_registry'; +import { ISavedObjectTypeRegistry, SavedObject } from 'src/core/server'; + +export type ISavedObjectsManagement = PublicMethodsOf; export class SavedObjectsManagement { constructor(private readonly registry: ISavedObjectTypeRegistry) {} - public getImportableAndExportableTypes() { - return this.registry - .getAllTypes() - .map(type => type.name) - .filter(type => this.isImportAndExportable(type)); - } - public isImportAndExportable(type: string) { return this.registry.isImportableAndExportable(type); } diff --git a/src/plugins/saved_objects_management/server/types.ts b/src/plugins/saved_objects_management/server/types.ts new file mode 100644 index 0000000000000..5c4763d357e87 --- /dev/null +++ b/src/plugins/saved_objects_management/server/types.ts @@ -0,0 +1,54 @@ +/* + * 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 } from 'src/core/server'; + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface SavedObjectsManagementPluginSetup {} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface SavedObjectsManagementPluginStart {} + +/** + * The metadata injected into a {@link SavedObject | saved object} when returning + * {@link SavedObjectWithMetadata | enhanced objects} from the plugin API endpoints. + */ +export interface SavedObjectMetadata { + icon?: string; + title?: string; + editUrl?: string; + inAppUrl?: { path: string; uiCapabilitiesPath: string }; +} + +/** + * A {@link SavedObject | saved object} enhanced with meta properties used by the client-side plugin. + */ +export type SavedObjectWithMetadata = SavedObject & { + meta: SavedObjectMetadata; +}; + +/** + * Represents a relation between two {@link SavedObject | saved object} + */ +export interface SavedObjectRelation { + id: string; + type: string; + relationship: 'child' | 'parent'; + meta: SavedObjectMetadata; +} diff --git a/test/api_integration/apis/index.js b/test/api_integration/apis/index.js index 57e9120773f33..c5bfc847d0041 100644 --- a/test/api_integration/apis/index.js +++ b/test/api_integration/apis/index.js @@ -25,7 +25,7 @@ export default function({ loadTestFile }) { loadTestFile(require.resolve('./home')); loadTestFile(require.resolve('./index_patterns')); loadTestFile(require.resolve('./kql_telemetry')); - loadTestFile(require.resolve('./management')); + loadTestFile(require.resolve('./saved_objects_management')); loadTestFile(require.resolve('./saved_objects')); loadTestFile(require.resolve('./scripts')); loadTestFile(require.resolve('./shorten')); diff --git a/test/api_integration/apis/management/saved_objects/relationships.js b/test/api_integration/apis/management/saved_objects/relationships.js deleted file mode 100644 index d202094e6d6f8..0000000000000 --- a/test/api_integration/apis/management/saved_objects/relationships.js +++ /dev/null @@ -1,449 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import expect from '@kbn/expect'; -const Joi = require('joi'); - -export default function({ getService }) { - const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); - - const GENERIC_RESPONSE_SCHEMA = Joi.array().items( - Joi.object().keys({ - id: Joi.string() - .uuid() - .required(), - type: Joi.string().required(), - relationship: Joi.string() - .valid('parent', 'child') - .required(), - meta: Joi.object() - .keys({ - title: Joi.string().required(), - icon: Joi.string().required(), - editUrl: Joi.string().required(), - inAppUrl: Joi.object() - .keys({ - path: Joi.string().required(), - uiCapabilitiesPath: Joi.string().required(), - }) - .required(), - }) - .required(), - }) - ); - - describe('relationships', () => { - before(() => esArchiver.load('management/saved_objects')); - after(() => esArchiver.unload('management/saved_objects')); - - const baseApiUrl = `/api/kibana/management/saved_objects/relationships`; - const coerceToArray = itemOrItems => [].concat(itemOrItems); - const getSavedObjectTypesQuery = types => - coerceToArray(types) - .map(type => `savedObjectTypes=${type}`) - .join('&'); - const defaultQuery = getSavedObjectTypesQuery([ - 'visualization', - 'index-pattern', - 'search', - 'dashboard', - ]); - - describe('searches', () => { - it('should validate search response schema', async () => { - await supertest - .get(`${baseApiUrl}/search/960372e0-3224-11e8-a572-ffca06da1357?${defaultQuery}`) - .expect(200) - .then(resp => { - const validationResult = Joi.validate(resp.body, GENERIC_RESPONSE_SCHEMA); - expect(validationResult.error).to.be(null); - }); - }); - - it('should work for searches', async () => { - await supertest - .get(`${baseApiUrl}/search/960372e0-3224-11e8-a572-ffca06da1357?${defaultQuery}`) - .expect(200) - .then(resp => { - expect(resp.body).to.eql([ - { - id: '8963ca30-3224-11e8-a572-ffca06da1357', - type: 'index-pattern', - relationship: 'child', - meta: { - title: 'saved_objects*', - icon: 'indexPatternApp', - editUrl: '/management/kibana/index_patterns/8963ca30-3224-11e8-a572-ffca06da1357', - inAppUrl: { - path: - '/app/kibana#/management/kibana/index_patterns/8963ca30-3224-11e8-a572-ffca06da1357', - uiCapabilitiesPath: 'management.kibana.index_patterns', - }, - }, - }, - { - id: 'a42c0580-3224-11e8-a572-ffca06da1357', - type: 'visualization', - relationship: 'parent', - meta: { - title: 'VisualizationFromSavedSearch', - icon: 'visualizeApp', - editUrl: - '/management/kibana/objects/savedVisualizations/a42c0580-3224-11e8-a572-ffca06da1357', - inAppUrl: { - path: '/app/kibana#/visualize/edit/a42c0580-3224-11e8-a572-ffca06da1357', - uiCapabilitiesPath: 'visualize.show', - }, - }, - }, - ]); - }); - }); - - it('should filter based on savedObjectTypes', async () => { - await supertest - .get( - `${baseApiUrl}/search/960372e0-3224-11e8-a572-ffca06da1357?${getSavedObjectTypesQuery( - 'visualization' - )}` - ) - .expect(200) - .then(resp => { - expect(resp.body).to.eql([ - { - id: '8963ca30-3224-11e8-a572-ffca06da1357', - type: 'index-pattern', - meta: { - icon: 'indexPatternApp', - title: 'saved_objects*', - editUrl: '/management/kibana/index_patterns/8963ca30-3224-11e8-a572-ffca06da1357', - inAppUrl: { - path: - '/app/kibana#/management/kibana/index_patterns/8963ca30-3224-11e8-a572-ffca06da1357', - uiCapabilitiesPath: 'management.kibana.index_patterns', - }, - }, - relationship: 'child', - }, - { - id: 'a42c0580-3224-11e8-a572-ffca06da1357', - type: 'visualization', - meta: { - icon: 'visualizeApp', - title: 'VisualizationFromSavedSearch', - editUrl: - '/management/kibana/objects/savedVisualizations/a42c0580-3224-11e8-a572-ffca06da1357', - inAppUrl: { - path: '/app/kibana#/visualize/edit/a42c0580-3224-11e8-a572-ffca06da1357', - uiCapabilitiesPath: 'visualize.show', - }, - }, - relationship: 'parent', - }, - ]); - }); - }); - - //TODO: https://github.com/elastic/kibana/issues/19713 causes this test to fail. - it.skip('should return 404 if search finds no results', async () => { - await supertest - .get(`${baseApiUrl}/search/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx${defaultQuery}`) - .expect(404); - }); - }); - - describe('dashboards', () => { - it('should validate dashboard response schema', async () => { - await supertest - .get(`${baseApiUrl}/dashboard/b70c7ae0-3224-11e8-a572-ffca06da1357?${defaultQuery}`) - .expect(200) - .then(resp => { - const validationResult = Joi.validate(resp.body, GENERIC_RESPONSE_SCHEMA); - expect(validationResult.error).to.be(null); - }); - }); - - it('should work for dashboards', async () => { - await supertest - .get(`${baseApiUrl}/dashboard/b70c7ae0-3224-11e8-a572-ffca06da1357?${defaultQuery}`) - .expect(200) - .then(resp => { - expect(resp.body).to.eql([ - { - id: 'add810b0-3224-11e8-a572-ffca06da1357', - type: 'visualization', - relationship: 'child', - meta: { - icon: 'visualizeApp', - title: 'Visualization', - editUrl: - '/management/kibana/objects/savedVisualizations/add810b0-3224-11e8-a572-ffca06da1357', - inAppUrl: { - path: '/app/kibana#/visualize/edit/add810b0-3224-11e8-a572-ffca06da1357', - uiCapabilitiesPath: 'visualize.show', - }, - }, - }, - { - id: 'a42c0580-3224-11e8-a572-ffca06da1357', - type: 'visualization', - relationship: 'child', - meta: { - icon: 'visualizeApp', - title: 'VisualizationFromSavedSearch', - editUrl: - '/management/kibana/objects/savedVisualizations/a42c0580-3224-11e8-a572-ffca06da1357', - inAppUrl: { - path: '/app/kibana#/visualize/edit/a42c0580-3224-11e8-a572-ffca06da1357', - uiCapabilitiesPath: 'visualize.show', - }, - }, - }, - ]); - }); - }); - - it('should filter based on savedObjectTypes', async () => { - await supertest - .get( - `${baseApiUrl}/dashboard/b70c7ae0-3224-11e8-a572-ffca06da1357?${getSavedObjectTypesQuery( - 'search' - )}` - ) - .expect(200) - .then(resp => { - expect(resp.body).to.eql([ - { - id: 'add810b0-3224-11e8-a572-ffca06da1357', - type: 'visualization', - meta: { - icon: 'visualizeApp', - title: 'Visualization', - editUrl: - '/management/kibana/objects/savedVisualizations/add810b0-3224-11e8-a572-ffca06da1357', - inAppUrl: { - path: '/app/kibana#/visualize/edit/add810b0-3224-11e8-a572-ffca06da1357', - uiCapabilitiesPath: 'visualize.show', - }, - }, - relationship: 'child', - }, - { - id: 'a42c0580-3224-11e8-a572-ffca06da1357', - type: 'visualization', - meta: { - icon: 'visualizeApp', - title: 'VisualizationFromSavedSearch', - editUrl: - '/management/kibana/objects/savedVisualizations/a42c0580-3224-11e8-a572-ffca06da1357', - inAppUrl: { - path: '/app/kibana#/visualize/edit/a42c0580-3224-11e8-a572-ffca06da1357', - uiCapabilitiesPath: 'visualize.show', - }, - }, - relationship: 'child', - }, - ]); - }); - }); - - //TODO: https://github.com/elastic/kibana/issues/19713 causes this test to fail. - it.skip('should return 404 if dashboard finds no results', async () => { - await supertest - .get(`${baseApiUrl}/dashboard/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx${defaultQuery}`) - .expect(404); - }); - }); - - describe('visualizations', () => { - it('should validate visualization response schema', async () => { - await supertest - .get(`${baseApiUrl}/visualization/a42c0580-3224-11e8-a572-ffca06da1357?${defaultQuery}`) - .expect(200) - .then(resp => { - const validationResult = Joi.validate(resp.body, GENERIC_RESPONSE_SCHEMA); - expect(validationResult.error).to.be(null); - }); - }); - - it('should work for visualizations', async () => { - await supertest - .get(`${baseApiUrl}/visualization/a42c0580-3224-11e8-a572-ffca06da1357?${defaultQuery}`) - .expect(200) - .then(resp => { - expect(resp.body).to.eql([ - { - id: '960372e0-3224-11e8-a572-ffca06da1357', - type: 'search', - relationship: 'child', - meta: { - icon: 'discoverApp', - title: 'OneRecord', - editUrl: - '/management/kibana/objects/savedSearches/960372e0-3224-11e8-a572-ffca06da1357', - inAppUrl: { - path: '/app/kibana#/discover/960372e0-3224-11e8-a572-ffca06da1357', - uiCapabilitiesPath: 'discover.show', - }, - }, - }, - { - id: 'b70c7ae0-3224-11e8-a572-ffca06da1357', - type: 'dashboard', - relationship: 'parent', - meta: { - icon: 'dashboardApp', - title: 'Dashboard', - editUrl: - '/management/kibana/objects/savedDashboards/b70c7ae0-3224-11e8-a572-ffca06da1357', - inAppUrl: { - path: '/app/kibana#/dashboard/b70c7ae0-3224-11e8-a572-ffca06da1357', - uiCapabilitiesPath: 'dashboard.show', - }, - }, - }, - ]); - }); - }); - - it('should filter based on savedObjectTypes', async () => { - await supertest - .get( - `${baseApiUrl}/visualization/a42c0580-3224-11e8-a572-ffca06da1357?${getSavedObjectTypesQuery( - 'search' - )}` - ) - .expect(200) - .then(resp => { - expect(resp.body).to.eql([ - { - id: '960372e0-3224-11e8-a572-ffca06da1357', - type: 'search', - meta: { - icon: 'discoverApp', - title: 'OneRecord', - editUrl: - '/management/kibana/objects/savedSearches/960372e0-3224-11e8-a572-ffca06da1357', - inAppUrl: { - path: '/app/kibana#/discover/960372e0-3224-11e8-a572-ffca06da1357', - uiCapabilitiesPath: 'discover.show', - }, - }, - relationship: 'child', - }, - ]); - }); - }); - - it('should return 404 if visualizations finds no results', async () => { - await supertest - .get(`${baseApiUrl}/visualization/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx?${defaultQuery}`) - .expect(404); - }); - }); - - describe('index patterns', () => { - it('should validate visualization response schema', async () => { - await supertest - .get(`${baseApiUrl}/index-pattern/8963ca30-3224-11e8-a572-ffca06da1357?${defaultQuery}`) - .expect(200) - .then(resp => { - const validationResult = Joi.validate(resp.body, GENERIC_RESPONSE_SCHEMA); - expect(validationResult.error).to.be(null); - }); - }); - - it('should work for index patterns', async () => { - await supertest - .get(`${baseApiUrl}/index-pattern/8963ca30-3224-11e8-a572-ffca06da1357?${defaultQuery}`) - .expect(200) - .then(resp => { - expect(resp.body).to.eql([ - { - id: '960372e0-3224-11e8-a572-ffca06da1357', - type: 'search', - relationship: 'parent', - meta: { - icon: 'discoverApp', - title: 'OneRecord', - editUrl: - '/management/kibana/objects/savedSearches/960372e0-3224-11e8-a572-ffca06da1357', - inAppUrl: { - path: '/app/kibana#/discover/960372e0-3224-11e8-a572-ffca06da1357', - uiCapabilitiesPath: 'discover.show', - }, - }, - }, - { - id: 'add810b0-3224-11e8-a572-ffca06da1357', - type: 'visualization', - relationship: 'parent', - meta: { - icon: 'visualizeApp', - title: 'Visualization', - editUrl: - '/management/kibana/objects/savedVisualizations/add810b0-3224-11e8-a572-ffca06da1357', - inAppUrl: { - path: '/app/kibana#/visualize/edit/add810b0-3224-11e8-a572-ffca06da1357', - uiCapabilitiesPath: 'visualize.show', - }, - }, - }, - ]); - }); - }); - - it('should filter based on savedObjectTypes', async () => { - await supertest - .get( - `${baseApiUrl}/index-pattern/8963ca30-3224-11e8-a572-ffca06da1357?${getSavedObjectTypesQuery( - 'search' - )}` - ) - .expect(200) - .then(resp => { - expect(resp.body).to.eql([ - { - id: '960372e0-3224-11e8-a572-ffca06da1357', - type: 'search', - meta: { - icon: 'discoverApp', - title: 'OneRecord', - editUrl: - '/management/kibana/objects/savedSearches/960372e0-3224-11e8-a572-ffca06da1357', - inAppUrl: { - path: '/app/kibana#/discover/960372e0-3224-11e8-a572-ffca06da1357', - uiCapabilitiesPath: 'discover.show', - }, - }, - relationship: 'parent', - }, - ]); - }); - }); - - it('should return 404 if index pattern finds no results', async () => { - await supertest - .get(`${baseApiUrl}/index-pattern/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx?${defaultQuery}`) - .expect(404); - }); - }); - }); -} diff --git a/test/api_integration/apis/management/saved_objects/find.js b/test/api_integration/apis/saved_objects_management/find.ts similarity index 89% rename from test/api_integration/apis/management/saved_objects/find.js rename to test/api_integration/apis/saved_objects_management/find.ts index 89e158671c3ca..9b3eda4c0664b 100644 --- a/test/api_integration/apis/management/saved_objects/find.js +++ b/test/api_integration/apis/saved_objects_management/find.ts @@ -18,8 +18,10 @@ */ import expect from '@kbn/expect'; +import { Response } from 'supertest'; +import { FtrProviderContext } from '../../ftr_provider_context'; -export default function({ getService }) { +export default function({ getService }: FtrProviderContext) { const es = getService('legacyEs'); const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); @@ -33,7 +35,7 @@ export default function({ getService }) { await supertest .get('/api/kibana/management/saved_objects/_find?type=visualization&fields=title') .expect(200) - .then(resp => { + .then((resp: Response) => { expect(resp.body).to.eql({ page: 1, per_page: 20, @@ -75,7 +77,7 @@ export default function({ getService }) { await supertest .get('/api/kibana/management/saved_objects/_find?type=wigwags') .expect(200) - .then(resp => { + .then((resp: Response) => { expect(resp.body).to.eql({ page: 1, per_page: 20, @@ -92,7 +94,7 @@ export default function({ getService }) { '/api/kibana/management/saved_objects/_find?type=visualization&page=100&perPage=100' ) .expect(200) - .then(resp => { + .then((resp: Response) => { expect(resp.body).to.eql({ page: 100, per_page: 100, @@ -107,15 +109,11 @@ export default function({ getService }) { await supertest .get('/api/kibana/management/saved_objects/_find?type=url&searchFields=a') .expect(400) - .then(resp => { + .then((resp: Response) => { expect(resp.body).to.eql({ statusCode: 400, error: 'Bad Request', - message: '"searchFields" is not allowed', - validation: { - source: 'query', - keys: ['searchFields'], - }, + message: '[request query.searchFields]: definition for this key is missing', }); })); }); @@ -135,7 +133,7 @@ export default function({ getService }) { await supertest .get('/api/kibana/management/saved_objects/_find?type=visualization') .expect(200) - .then(resp => { + .then((resp: Response) => { expect(resp.body).to.eql({ page: 1, per_page: 20, @@ -149,7 +147,7 @@ export default function({ getService }) { await supertest .get('/api/kibana/management/saved_objects/_find?type=wigwags') .expect(200) - .then(resp => { + .then((resp: Response) => { expect(resp.body).to.eql({ page: 1, per_page: 20, @@ -164,15 +162,12 @@ export default function({ getService }) { await supertest .get('/api/kibana/management/saved_objects/_find') .expect(400) - .then(resp => { + .then((resp: Response) => { expect(resp.body).to.eql({ error: 'Bad Request', - message: 'child "type" fails because ["type" is required]', + message: + '[request query.type]: expected at least one defined value but got [undefined]', statusCode: 400, - validation: { - keys: ['type'], - source: 'query', - }, }); })); }); @@ -184,7 +179,7 @@ export default function({ getService }) { '/api/kibana/management/saved_objects/_find?type=visualization&page=100&perPage=100' ) .expect(200) - .then(resp => { + .then((resp: Response) => { expect(resp.body).to.eql({ page: 100, per_page: 100, @@ -199,29 +194,25 @@ export default function({ getService }) { await supertest .get('/api/kibana/management/saved_objects/_find?type=url&searchFields=a') .expect(400) - .then(resp => { + .then((resp: Response) => { expect(resp.body).to.eql({ statusCode: 400, error: 'Bad Request', - message: '"searchFields" is not allowed', - validation: { - source: 'query', - keys: ['searchFields'], - }, + message: '[request query.searchFields]: definition for this key is missing', }); })); }); }); describe('meta attributes injected properly', () => { - before(() => esArchiver.load('management/saved_objects')); - after(() => esArchiver.unload('management/saved_objects')); + before(() => esArchiver.load('management/saved_objects/search')); + after(() => esArchiver.unload('management/saved_objects/search')); it('should inject meta attributes for searches', async () => await supertest .get('/api/kibana/management/saved_objects/_find?type=search') .expect(200) - .then(resp => { + .then((resp: Response) => { expect(resp.body.saved_objects).to.have.length(1); expect(resp.body.saved_objects[0].meta).to.eql({ icon: 'discoverApp', @@ -239,7 +230,7 @@ export default function({ getService }) { await supertest .get('/api/kibana/management/saved_objects/_find?type=dashboard') .expect(200) - .then(resp => { + .then((resp: Response) => { expect(resp.body.saved_objects).to.have.length(1); expect(resp.body.saved_objects[0].meta).to.eql({ icon: 'dashboardApp', @@ -257,7 +248,7 @@ export default function({ getService }) { await supertest .get('/api/kibana/management/saved_objects/_find?type=visualization') .expect(200) - .then(resp => { + .then((resp: Response) => { expect(resp.body.saved_objects).to.have.length(2); expect(resp.body.saved_objects[0].meta).to.eql({ icon: 'visualizeApp', @@ -285,7 +276,7 @@ export default function({ getService }) { await supertest .get('/api/kibana/management/saved_objects/_find?type=index-pattern') .expect(200) - .then(resp => { + .then((resp: Response) => { expect(resp.body.saved_objects).to.have.length(1); expect(resp.body.saved_objects[0].meta).to.eql({ icon: 'indexPatternApp', diff --git a/src/legacy/core_plugins/kibana/server/routes/api/management/index.js b/test/api_integration/apis/saved_objects_management/index.ts similarity index 66% rename from src/legacy/core_plugins/kibana/server/routes/api/management/index.js rename to test/api_integration/apis/saved_objects_management/index.ts index b98ce360f57d3..5895940c4bbef 100644 --- a/src/legacy/core_plugins/kibana/server/routes/api/management/index.js +++ b/test/api_integration/apis/saved_objects_management/index.ts @@ -17,13 +17,12 @@ * under the License. */ -import { registerFind } from './saved_objects/find'; -import { registerRelationships } from './saved_objects/relationships'; -import { registerScrollForExportRoute, registerScrollForCountRoute } from './saved_objects/scroll'; +import { FtrProviderContext } from '../../ftr_provider_context'; -export function managementApi(server) { - registerRelationships(server); - registerFind(server); - registerScrollForExportRoute(server); - registerScrollForCountRoute(server); +export default function({ loadTestFile }: FtrProviderContext) { + describe('saved objects management apis', () => { + loadTestFile(require.resolve('./find')); + loadTestFile(require.resolve('./relationships')); + loadTestFile(require.resolve('./scroll_count')); + }); } diff --git a/test/api_integration/apis/saved_objects_management/relationships.ts b/test/api_integration/apis/saved_objects_management/relationships.ts new file mode 100644 index 0000000000000..78a437ab56cf7 --- /dev/null +++ b/test/api_integration/apis/saved_objects_management/relationships.ts @@ -0,0 +1,423 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import expect from '@kbn/expect'; +import { schema } from '@kbn/config-schema'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + const responseSchema = schema.arrayOf( + schema.object({ + id: schema.string(), + type: schema.string(), + relationship: schema.oneOf([schema.literal('parent'), schema.literal('child')]), + meta: schema.object({ + title: schema.string(), + icon: schema.string(), + editUrl: schema.string(), + inAppUrl: schema.object({ + path: schema.string(), + uiCapabilitiesPath: schema.string(), + }), + }), + }) + ); + + describe('relationships', () => { + before(async () => { + await esArchiver.load('management/saved_objects/relationships'); + }); + after(async () => { + await esArchiver.unload('management/saved_objects/relationships'); + }); + + const baseApiUrl = `/api/kibana/management/saved_objects/relationships`; + const defaultTypes = ['visualization', 'index-pattern', 'search', 'dashboard']; + + const relationshipsUrl = (type: string, id: string, types: string[] = defaultTypes) => { + const typesQuery = types.map(t => `savedObjectTypes=${t}`).join('&'); + return `${baseApiUrl}/${type}/${id}?${typesQuery}`; + }; + + describe('searches', () => { + it('should validate search response schema', async () => { + const resp = await supertest + .get(relationshipsUrl('search', '960372e0-3224-11e8-a572-ffca06da1357')) + .expect(200); + + expect(() => { + responseSchema.validate(resp.body); + }).not.to.throwError(); + }); + + it('should work for searches', async () => { + const resp = await supertest + .get(relationshipsUrl('search', '960372e0-3224-11e8-a572-ffca06da1357')) + .expect(200); + + expect(resp.body).to.eql([ + { + id: '8963ca30-3224-11e8-a572-ffca06da1357', + type: 'index-pattern', + relationship: 'child', + meta: { + title: 'saved_objects*', + icon: 'indexPatternApp', + editUrl: '/management/kibana/index_patterns/8963ca30-3224-11e8-a572-ffca06da1357', + inAppUrl: { + path: + '/app/kibana#/management/kibana/index_patterns/8963ca30-3224-11e8-a572-ffca06da1357', + uiCapabilitiesPath: 'management.kibana.index_patterns', + }, + }, + }, + { + id: 'a42c0580-3224-11e8-a572-ffca06da1357', + type: 'visualization', + relationship: 'parent', + meta: { + title: 'VisualizationFromSavedSearch', + icon: 'visualizeApp', + editUrl: + '/management/kibana/objects/savedVisualizations/a42c0580-3224-11e8-a572-ffca06da1357', + inAppUrl: { + path: '/app/kibana#/visualize/edit/a42c0580-3224-11e8-a572-ffca06da1357', + uiCapabilitiesPath: 'visualize.show', + }, + }, + }, + ]); + }); + + it('should filter based on savedObjectTypes', async () => { + const resp = await supertest + .get( + relationshipsUrl('search', '960372e0-3224-11e8-a572-ffca06da1357', ['visualization']) + ) + .expect(200); + + expect(resp.body).to.eql([ + { + id: '8963ca30-3224-11e8-a572-ffca06da1357', + type: 'index-pattern', + meta: { + icon: 'indexPatternApp', + title: 'saved_objects*', + editUrl: '/management/kibana/index_patterns/8963ca30-3224-11e8-a572-ffca06da1357', + inAppUrl: { + path: + '/app/kibana#/management/kibana/index_patterns/8963ca30-3224-11e8-a572-ffca06da1357', + uiCapabilitiesPath: 'management.kibana.index_patterns', + }, + }, + relationship: 'child', + }, + { + id: 'a42c0580-3224-11e8-a572-ffca06da1357', + type: 'visualization', + meta: { + icon: 'visualizeApp', + title: 'VisualizationFromSavedSearch', + editUrl: + '/management/kibana/objects/savedVisualizations/a42c0580-3224-11e8-a572-ffca06da1357', + inAppUrl: { + path: '/app/kibana#/visualize/edit/a42c0580-3224-11e8-a572-ffca06da1357', + uiCapabilitiesPath: 'visualize.show', + }, + }, + relationship: 'parent', + }, + ]); + }); + + // TODO: https://github.com/elastic/kibana/issues/19713 causes this test to fail. + it.skip('should return 404 if search finds no results', async () => { + await supertest + .get(relationshipsUrl('search', 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx')) + .expect(404); + }); + }); + + describe('dashboards', () => { + it('should validate dashboard response schema', async () => { + const resp = await supertest + .get(relationshipsUrl('dashboard', 'b70c7ae0-3224-11e8-a572-ffca06da1357')) + .expect(200); + + expect(() => { + responseSchema.validate(resp.body); + }).not.to.throwError(); + }); + + it('should work for dashboards', async () => { + const resp = await supertest + .get(relationshipsUrl('dashboard', 'b70c7ae0-3224-11e8-a572-ffca06da1357')) + .expect(200); + + expect(resp.body).to.eql([ + { + id: 'add810b0-3224-11e8-a572-ffca06da1357', + type: 'visualization', + relationship: 'child', + meta: { + icon: 'visualizeApp', + title: 'Visualization', + editUrl: + '/management/kibana/objects/savedVisualizations/add810b0-3224-11e8-a572-ffca06da1357', + inAppUrl: { + path: '/app/kibana#/visualize/edit/add810b0-3224-11e8-a572-ffca06da1357', + uiCapabilitiesPath: 'visualize.show', + }, + }, + }, + { + id: 'a42c0580-3224-11e8-a572-ffca06da1357', + type: 'visualization', + relationship: 'child', + meta: { + icon: 'visualizeApp', + title: 'VisualizationFromSavedSearch', + editUrl: + '/management/kibana/objects/savedVisualizations/a42c0580-3224-11e8-a572-ffca06da1357', + inAppUrl: { + path: '/app/kibana#/visualize/edit/a42c0580-3224-11e8-a572-ffca06da1357', + uiCapabilitiesPath: 'visualize.show', + }, + }, + }, + ]); + }); + + it('should filter based on savedObjectTypes', async () => { + const resp = await supertest + .get(relationshipsUrl('dashboard', 'b70c7ae0-3224-11e8-a572-ffca06da1357', ['search'])) + .expect(200); + + expect(resp.body).to.eql([ + { + id: 'add810b0-3224-11e8-a572-ffca06da1357', + type: 'visualization', + meta: { + icon: 'visualizeApp', + title: 'Visualization', + editUrl: + '/management/kibana/objects/savedVisualizations/add810b0-3224-11e8-a572-ffca06da1357', + inAppUrl: { + path: '/app/kibana#/visualize/edit/add810b0-3224-11e8-a572-ffca06da1357', + uiCapabilitiesPath: 'visualize.show', + }, + }, + relationship: 'child', + }, + { + id: 'a42c0580-3224-11e8-a572-ffca06da1357', + type: 'visualization', + meta: { + icon: 'visualizeApp', + title: 'VisualizationFromSavedSearch', + editUrl: + '/management/kibana/objects/savedVisualizations/a42c0580-3224-11e8-a572-ffca06da1357', + inAppUrl: { + path: '/app/kibana#/visualize/edit/a42c0580-3224-11e8-a572-ffca06da1357', + uiCapabilitiesPath: 'visualize.show', + }, + }, + relationship: 'child', + }, + ]); + }); + + // TODO: https://github.com/elastic/kibana/issues/19713 causes this test to fail. + it.skip('should return 404 if dashboard finds no results', async () => { + await supertest + .get(relationshipsUrl('dashboard', 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx')) + .expect(404); + }); + }); + + describe('visualizations', () => { + it('should validate visualization response schema', async () => { + const resp = await supertest + .get(relationshipsUrl('visualization', 'a42c0580-3224-11e8-a572-ffca06da1357')) + .expect(200); + + expect(() => { + responseSchema.validate(resp.body); + }).not.to.throwError(); + }); + + it('should work for visualizations', async () => { + const resp = await supertest + .get(relationshipsUrl('visualization', 'a42c0580-3224-11e8-a572-ffca06da1357')) + .expect(200); + + expect(resp.body).to.eql([ + { + id: '960372e0-3224-11e8-a572-ffca06da1357', + type: 'search', + relationship: 'child', + meta: { + icon: 'discoverApp', + title: 'OneRecord', + editUrl: + '/management/kibana/objects/savedSearches/960372e0-3224-11e8-a572-ffca06da1357', + inAppUrl: { + path: '/app/kibana#/discover/960372e0-3224-11e8-a572-ffca06da1357', + uiCapabilitiesPath: 'discover.show', + }, + }, + }, + { + id: 'b70c7ae0-3224-11e8-a572-ffca06da1357', + type: 'dashboard', + relationship: 'parent', + meta: { + icon: 'dashboardApp', + title: 'Dashboard', + editUrl: + '/management/kibana/objects/savedDashboards/b70c7ae0-3224-11e8-a572-ffca06da1357', + inAppUrl: { + path: '/app/kibana#/dashboard/b70c7ae0-3224-11e8-a572-ffca06da1357', + uiCapabilitiesPath: 'dashboard.show', + }, + }, + }, + ]); + }); + + it('should filter based on savedObjectTypes', async () => { + const resp = await supertest + .get( + relationshipsUrl('visualization', 'a42c0580-3224-11e8-a572-ffca06da1357', ['search']) + ) + .expect(200); + + expect(resp.body).to.eql([ + { + id: '960372e0-3224-11e8-a572-ffca06da1357', + type: 'search', + meta: { + icon: 'discoverApp', + title: 'OneRecord', + editUrl: + '/management/kibana/objects/savedSearches/960372e0-3224-11e8-a572-ffca06da1357', + inAppUrl: { + path: '/app/kibana#/discover/960372e0-3224-11e8-a572-ffca06da1357', + uiCapabilitiesPath: 'discover.show', + }, + }, + relationship: 'child', + }, + ]); + }); + + it('should return 404 if visualizations finds no results', async () => { + await supertest + .get(relationshipsUrl('visualization', 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx')) + .expect(404); + }); + }); + + describe('index patterns', () => { + it('should validate visualization response schema', async () => { + const resp = await supertest + .get(relationshipsUrl('index-pattern', '8963ca30-3224-11e8-a572-ffca06da1357')) + .expect(200); + + expect(() => { + responseSchema.validate(resp.body); + }).not.to.throwError(); + }); + + it('should work for index patterns', async () => { + const resp = await supertest + .get(relationshipsUrl('index-pattern', '8963ca30-3224-11e8-a572-ffca06da1357')) + .expect(200); + + expect(resp.body).to.eql([ + { + id: '960372e0-3224-11e8-a572-ffca06da1357', + type: 'search', + relationship: 'parent', + meta: { + icon: 'discoverApp', + title: 'OneRecord', + editUrl: + '/management/kibana/objects/savedSearches/960372e0-3224-11e8-a572-ffca06da1357', + inAppUrl: { + path: '/app/kibana#/discover/960372e0-3224-11e8-a572-ffca06da1357', + uiCapabilitiesPath: 'discover.show', + }, + }, + }, + { + id: 'add810b0-3224-11e8-a572-ffca06da1357', + type: 'visualization', + relationship: 'parent', + meta: { + icon: 'visualizeApp', + title: 'Visualization', + editUrl: + '/management/kibana/objects/savedVisualizations/add810b0-3224-11e8-a572-ffca06da1357', + inAppUrl: { + path: '/app/kibana#/visualize/edit/add810b0-3224-11e8-a572-ffca06da1357', + uiCapabilitiesPath: 'visualize.show', + }, + }, + }, + ]); + }); + + it('should filter based on savedObjectTypes', async () => { + const resp = await supertest + .get( + relationshipsUrl('index-pattern', '8963ca30-3224-11e8-a572-ffca06da1357', ['search']) + ) + .expect(200); + + expect(resp.body).to.eql([ + { + id: '960372e0-3224-11e8-a572-ffca06da1357', + type: 'search', + meta: { + icon: 'discoverApp', + title: 'OneRecord', + editUrl: + '/management/kibana/objects/savedSearches/960372e0-3224-11e8-a572-ffca06da1357', + inAppUrl: { + path: '/app/kibana#/discover/960372e0-3224-11e8-a572-ffca06da1357', + uiCapabilitiesPath: 'discover.show', + }, + }, + relationship: 'parent', + }, + ]); + }); + + it('should return 404 if index pattern finds no results', async () => { + await supertest + .get(relationshipsUrl('index-pattern', 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx')) + .expect(404); + }); + }); + }); +} diff --git a/test/api_integration/apis/saved_objects_management/scroll_count.ts b/test/api_integration/apis/saved_objects_management/scroll_count.ts new file mode 100644 index 0000000000000..3c29d45244dec --- /dev/null +++ b/test/api_integration/apis/saved_objects_management/scroll_count.ts @@ -0,0 +1,102 @@ +/* + * 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 { SuperTest, Test } from 'supertest'; +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +const apiUrl = '/api/kibana/management/saved_objects/scroll/counts'; +const defaultTypes = ['visualization', 'index-pattern', 'search', 'dashboard']; + +export default function({ getService }: FtrProviderContext) { + const supertest = getService('supertest') as SuperTest; + const esArchiver = getService('esArchiver'); + + describe('scroll_count', () => { + before(async () => { + await esArchiver.load('management/saved_objects/scroll_count'); + }); + after(async () => { + await esArchiver.unload('management/saved_objects/scroll_count'); + }); + + it('returns the count for each included types', async () => { + const res = await supertest + .post(apiUrl) + .send({ + typesToInclude: defaultTypes, + }) + .expect(200); + + expect(res.body).to.eql({ + dashboard: 2, + 'index-pattern': 1, + search: 1, + visualization: 2, + }); + }); + + it('only returns count for types to include', async () => { + const res = await supertest + .post(apiUrl) + .send({ + typesToInclude: ['dashboard', 'search'], + }) + .expect(200); + + expect(res.body).to.eql({ + dashboard: 2, + search: 1, + }); + }); + + it('filters on title when `searchString` is provided', async () => { + const res = await supertest + .post(apiUrl) + .send({ + typesToInclude: defaultTypes, + searchString: 'Amazing', + }) + .expect(200); + + expect(res.body).to.eql({ + dashboard: 1, + visualization: 1, + 'index-pattern': 0, + search: 0, + }); + }); + + it('includes all requested types even when none match the search', async () => { + const res = await supertest + .post(apiUrl) + .send({ + typesToInclude: ['dashboard', 'search', 'visualization'], + searchString: 'nothing-will-match', + }) + .expect(200); + + expect(res.body).to.eql({ + dashboard: 0, + visualization: 0, + search: 0, + }); + }); + }); +} diff --git a/test/api_integration/fixtures/es_archiver/management/saved_objects/data.json.gz b/test/api_integration/fixtures/es_archiver/management/saved_objects/relationships/data.json.gz similarity index 100% rename from test/api_integration/fixtures/es_archiver/management/saved_objects/data.json.gz rename to test/api_integration/fixtures/es_archiver/management/saved_objects/relationships/data.json.gz diff --git a/test/api_integration/fixtures/es_archiver/management/saved_objects/mappings.json b/test/api_integration/fixtures/es_archiver/management/saved_objects/relationships/mappings.json similarity index 100% rename from test/api_integration/fixtures/es_archiver/management/saved_objects/mappings.json rename to test/api_integration/fixtures/es_archiver/management/saved_objects/relationships/mappings.json diff --git a/test/api_integration/fixtures/es_archiver/management/saved_objects/scroll_count/data.json.gz b/test/api_integration/fixtures/es_archiver/management/saved_objects/scroll_count/data.json.gz new file mode 100644 index 0000000000000..1c327e7e0769b Binary files /dev/null and b/test/api_integration/fixtures/es_archiver/management/saved_objects/scroll_count/data.json.gz differ diff --git a/test/api_integration/fixtures/es_archiver/management/saved_objects/scroll_count/mappings.json b/test/api_integration/fixtures/es_archiver/management/saved_objects/scroll_count/mappings.json new file mode 100644 index 0000000000000..8270c573e4c1e --- /dev/null +++ b/test/api_integration/fixtures/es_archiver/management/saved_objects/scroll_count/mappings.json @@ -0,0 +1,213 @@ +{ + "type": "index", + "value": { + "index": ".kibana", + "settings": { + "index": { + "number_of_shards": "1", + "auto_expand_replicas": "0-1", + "number_of_replicas": "0" + } + }, + "mappings": { + "dynamic": "strict", + "properties": { + "config": { + "dynamic": "true", + "properties": { + "buildNum": { + "type": "keyword" + }, + "defaultIndex": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "telemetry:optIn": { + "type": "boolean" + } + } + }, + "dashboard": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "optionsJSON": { + "type": "text" + }, + "panelsJSON": { + "type": "text" + }, + "refreshInterval": { + "properties": { + "display": { + "type": "keyword" + }, + "pause": { + "type": "boolean" + }, + "section": { + "type": "integer" + }, + "value": { + "type": "integer" + } + } + }, + "timeFrom": { + "type": "keyword" + }, + "timeRestore": { + "type": "boolean" + }, + "timeTo": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "index-pattern": { + "properties": { + "fieldFormatMap": { + "type": "text" + }, + "fields": { + "type": "text" + }, + "intervalName": { + "type": "keyword" + }, + "notExpandable": { + "type": "boolean" + }, + "sourceFilters": { + "type": "text" + }, + "timeFieldName": { + "type": "keyword" + }, + "title": { + "type": "text" + } + } + }, + "search": { + "properties": { + "columns": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "sort": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "server": { + "properties": { + "uuid": { + "type": "keyword" + } + } + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "url": { + "properties": { + "accessCount": { + "type": "long" + }, + "accessDate": { + "type": "date" + }, + "createDate": { + "type": "date" + }, + "url": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 2048 + } + } + } + } + }, + "visualization": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "savedSearchId": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "visState": { + "type": "text" + } + } + } + } + } + } +} diff --git a/test/api_integration/fixtures/es_archiver/management/saved_objects/search/data.json.gz b/test/api_integration/fixtures/es_archiver/management/saved_objects/search/data.json.gz new file mode 100644 index 0000000000000..0834567abb66b Binary files /dev/null and b/test/api_integration/fixtures/es_archiver/management/saved_objects/search/data.json.gz differ diff --git a/test/api_integration/fixtures/es_archiver/management/saved_objects/search/mappings.json b/test/api_integration/fixtures/es_archiver/management/saved_objects/search/mappings.json new file mode 100644 index 0000000000000..c670508247b1a --- /dev/null +++ b/test/api_integration/fixtures/es_archiver/management/saved_objects/search/mappings.json @@ -0,0 +1,283 @@ +{ + "type": "index", + "value": { + "index": ".kibana", + "settings": { + "index": { + "number_of_shards": "1", + "auto_expand_replicas": "0-1", + "number_of_replicas": "0" + } + }, + "mappings": { + "dynamic": "strict", + "properties": { + "config": { + "dynamic": "true", + "properties": { + "buildNum": { + "type": "keyword" + }, + "defaultIndex": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "telemetry:optIn": { + "type": "boolean" + } + } + }, + "dashboard": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "optionsJSON": { + "type": "text" + }, + "panelsJSON": { + "type": "text" + }, + "refreshInterval": { + "properties": { + "display": { + "type": "keyword" + }, + "pause": { + "type": "boolean" + }, + "section": { + "type": "integer" + }, + "value": { + "type": "integer" + } + } + }, + "timeFrom": { + "type": "keyword" + }, + "timeRestore": { + "type": "boolean" + }, + "timeTo": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "graph-workspace": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "numLinks": { + "type": "integer" + }, + "numVertices": { + "type": "integer" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "wsState": { + "type": "text" + } + } + }, + "index-pattern": { + "properties": { + "fieldFormatMap": { + "type": "text" + }, + "fields": { + "type": "text" + }, + "intervalName": { + "type": "keyword" + }, + "notExpandable": { + "type": "boolean" + }, + "sourceFilters": { + "type": "text" + }, + "timeFieldName": { + "type": "keyword" + }, + "title": { + "type": "text" + } + } + }, + "search": { + "properties": { + "columns": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "sort": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "server": { + "properties": { + "uuid": { + "type": "keyword" + } + } + }, + "timelion-sheet": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "timelion_chart_height": { + "type": "integer" + }, + "timelion_columns": { + "type": "integer" + }, + "timelion_interval": { + "type": "keyword" + }, + "timelion_other_interval": { + "type": "keyword" + }, + "timelion_rows": { + "type": "integer" + }, + "timelion_sheet": { + "type": "text" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "url": { + "properties": { + "accessCount": { + "type": "long" + }, + "accessDate": { + "type": "date" + }, + "createDate": { + "type": "date" + }, + "url": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 2048 + } + } + } + } + }, + "visualization": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "savedSearchId": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "visState": { + "type": "text" + } + } + } + } + } + } +} \ No newline at end of file