Migrate SO management routes to new plugin (#59734) (#61439)

* unexpose SavedObjectsManagement from legacy server

* migrate saved object management routes to new plugin

* fix endpoint methods

* adapt code due to rebase

* extract types

* improve findAll params

* adapt existing api integration tests and migrate to TS

* update generated doc

* add API integration tests for /scroll/count

* add unit tests for plugin and routes

* add injectMetaAttributes tests

* extract relation type

* add find_relationships tests

* add find_all tests

* do not complete migrator$ to avoid unhandled promise rejection

* fix data for search endpoint integration tests

* remove falsy comment

* rename plugin folder to match plugin id

* address review comments

* update CODEOWNERS
This commit is contained in:
Pierre Gayvallet 2020-03-26 14:32:43 +01:00 committed by GitHub
parent 985cea7797
commit 557c72a9da
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
47 changed files with 2217 additions and 917 deletions

View file

@ -21,8 +21,6 @@ export * from './service';
export { SavedObjectsSchema } from './schema';
export { SavedObjectsManagement } from './management';
export * from './import';
export {

View file

@ -36,6 +36,7 @@ export interface SavedObjectsLegacyService {
getScopedSavedObjectsClient: SavedObjectsClientProvider['getClient'];
SavedObjectsClient: typeof SavedObjectsClient;
types: string[];
importAndExportableTypes: string[];
schema: SavedObjectsSchema;
getSavedObjectsRepository(...rest: any[]): any;
importExport: {

View file

@ -2028,6 +2028,8 @@ export interface SavedObjectsLegacyService {
// (undocumented)
getScopedSavedObjectsClient: SavedObjectsClientProvider['getClient'];
// (undocumented)
importAndExportableTypes: string[];
// (undocumented)
importExport: {
objectLimit: number;
importSavedObjects(options: SavedObjectsImportOptions): Promise<SavedObjectsImportResponse>;

View file

@ -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));
},

View file

@ -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,

View file

@ -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;
}),
};
},
});
}

View file

@ -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,
});
},
});
}

View file

@ -1,122 +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 => {
const type = hit.type;
return {
_id: hit.id,
_type: type,
_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;
},
});
}

View file

@ -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'],
});
});
});

View file

@ -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: (

View file

@ -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,

View file

@ -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();
});
});

View file

@ -0,0 +1,6 @@
{
"id": "savedObjectsManagement",
"version": "kibana",
"server": true,
"ui": false
}

View file

@ -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';

View file

@ -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<typeof savedObjectsClientMock.create>;
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)));
});
});

View file

@ -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<SavedObject[]> => {
return recursiveFind(client, findOptions, 1, []);
};
const recursiveFind = async (
client: SavedObjectsClientContract,
findOptions: SavedObjectsFindOptions,
page: number,
allObjects: SavedObject[]
): Promise<SavedObject[]> => {
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;
};

View file

@ -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<typeof savedObjectsClientMock.create>;
let managementService: ReturnType<typeof managementMock.create>;
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',
},
},
},
]);
});
});

View file

@ -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<SavedObjectRelation[]> {
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,
};
}

View file

@ -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';

View file

@ -0,0 +1,82 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { SavedObject } from 'src/core/server';
import { injectMetaAttributes } from './inject_meta_attributes';
import { managementMock } from '../services/management.mock';
describe('injectMetaAttributes', () => {
let managementService: ReturnType<typeof managementMock.create>;
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<any> = {
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<any> = {
id: 'id',
type: 'config',
attributes: { some: 'value' },
references: [],
};
injectMetaAttributes(obj, managementService);
expect(obj).toStrictEqual({
id: 'id',
type: 'config',
attributes: { some: 'value' },
references: [],
});
});
});

View file

@ -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<T = unknown>(
savedObject: SavedObject<T> | SavedObjectWithMetadata<T>,
savedObjectsManagement: ISavedObjectsManagement
): SavedObjectWithMetadata<T> {
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;
}

View file

@ -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,
}));

View file

@ -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,
})
);
});
});
});

View file

@ -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<SavedObjectsManagementPluginSetup, SavedObjectsManagementPluginStart, {}, {}> {
private readonly logger: Logger;
private managementService$ = new Subject<SavedObjectsManagement>();
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 {};
}
}

View file

@ -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<ISavedObjectsManagement>
) => {
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<string>();
importAndExportableTypes.forEach(type => {
const searchField = managementService.getDefaultSearchField(type);
if (searchField) {
searchFields.add(searchField);
}
});
const findResponse = await client.find<any>({
...req.query,
fields: undefined,
searchFields: [...searchFields],
});
const enhancedSavedObjects = findResponse.saved_objects
.map(so => injectMetaAttributes(so, managementService))
.map(obj => {
const result = { ...obj, attributes: {} as Record<string, any> };
for (const field of includedFields) {
result.attributes[field] = obj.attributes[field];
}
return result;
});
return res.ok({
body: {
...findResponse,
saved_objects: enhancedSavedObjects,
},
});
})
);
};

View file

@ -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)
);
});
});

View file

@ -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<ISavedObjectsManagement>;
}
export function registerRoutes({ http, managementServicePromise }: RegisterRouteOptions) {
const router = http.createRouter();
registerFindRoute(router, managementServicePromise);
registerScrollForCountRoute(router);
registerScrollForExportRoute(router);
registerRelationshipsRoute(router, managementServicePromise);
}

View file

@ -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<ISavedObjectsManagement>
) => {
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,
});
})
);
};

View file

@ -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<string, number>);
for (const type of req.body.typesToInclude) {
if (!counts[type]) {
counts[type] = 0;
}
}
return res.ok({
body: counts,
});
})
);
};

View file

@ -0,0 +1,57 @@
/*
* 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,
_type: hit.type,
_source: hit.attributes,
_meta: {
savedObjectVersion: 2,
},
_migrationVersion: hit.migrationVersion,
_references: hit.references || [],
};
}),
});
})
);
};

View file

@ -17,4 +17,4 @@
* under the License.
*/
export { SavedObjectsManagement } from './management';
export { SavedObjectsManagement, ISavedObjectsManagement } from './management';

View file

@ -24,7 +24,6 @@ const createManagementMock = () => {
const mocked: jest.Mocked<Management> = {
isImportAndExportable: jest.fn().mockReturnValue(true),
getDefaultSearchField: jest.fn(),
getImportableAndExportableTypes: jest.fn(),
getIcon: jest.fn(),
getTitle: jest.fn(),
getEditUrl: jest.fn(),

View file

@ -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;

View file

@ -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<SavedObjectsManagement>;
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);
}

View file

@ -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<T = unknown> = SavedObject<T> & {
meta: SavedObjectMetadata;
};
/**
* Represents a relation between two {@link SavedObject | saved object}
*/
export interface SavedObjectRelation {
id: string;
type: string;
relationship: 'child' | 'parent';
meta: SavedObjectMetadata;
}

View file

@ -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'));

View file

@ -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);
});
});
});
}

View file

@ -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',

View file

@ -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'));
});
}

View file

@ -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);
});
});
});
}

View file

@ -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<Test>;
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,
});
});
});
}

View file

@ -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"
}
}
}
}
}
}
}

View file

@ -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"
}
}
}
}
}
}
}