Migrate legacy import/export endpoints (#69474)

* migrate legacy export routes to `legacy_export` plugin

* adapt unit tests

* remove already dead (already moved) libs
This commit is contained in:
Pierre Gayvallet 2020-06-24 10:21:27 +02:00 committed by GitHub
parent b270321ff3
commit 36b66a802e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 298 additions and 1055 deletions

View file

@ -21,8 +21,6 @@ import Fs from 'fs';
import { resolve } from 'path';
import { promisify } from 'util';
import { importApi } from './server/routes/api/import';
import { exportApi } from './server/routes/api/export';
import { getUiSettingDefaults } from './server/ui_setting_defaults';
import { registerCspCollector } from './server/lib/csp_usage_collector';
import { injectVars } from './inject_vars';
@ -91,9 +89,6 @@ export default function (kibana) {
init: async function (server) {
const { usageCollection } = server.newPlatform.setup.plugins;
// routes
importApi(server);
exportApi(server);
registerCspCollector(usageCollection, server);
server.injectUiAppVars('kibana', () => injectVars(server));
},

View file

@ -1,645 +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';
import { findRelationships } from '../management/saved_objects/relationships';
function getManagementaMock(savedObjectSchemas) {
return {
isImportAndExportable(type) {
return (
!savedObjectSchemas[type] || savedObjectSchemas[type].isImportableAndExportable !== false
);
},
getDefaultSearchField(type) {
return savedObjectSchemas[type] && savedObjectSchemas[type].defaultSearchField;
},
getIcon(type) {
return savedObjectSchemas[type] && savedObjectSchemas[type].icon;
},
getTitle(savedObject) {
const { type } = savedObject;
const getTitle = savedObjectSchemas[type] && savedObjectSchemas[type].getTitle;
if (getTitle) {
return getTitle(savedObject);
}
},
getEditUrl(savedObject) {
const { type } = savedObject;
const getEditUrl = savedObjectSchemas[type] && savedObjectSchemas[type].getEditUrl;
if (getEditUrl) {
return getEditUrl(savedObject);
}
},
getInAppUrl(savedObject) {
const { type } = savedObject;
const getInAppUrl = savedObjectSchemas[type] && savedObjectSchemas[type].getInAppUrl;
if (getInAppUrl) {
return getInAppUrl(savedObject);
}
},
};
}
const savedObjectsManagement = getManagementaMock({
'index-pattern': {
icon: 'indexPatternApp',
defaultSearchField: 'title',
getTitle(obj) {
return obj.attributes.title;
},
getEditUrl(obj) {
return `/management/kibana/indexPatterns/patterns/${encodeURIComponent(obj.id)}`;
},
getInAppUrl(obj) {
return {
path: `/app/management/kibana/indexPatterns/patterns/${encodeURIComponent(obj.id)}`,
uiCapabilitiesPath: 'management.kibana.index_patterns',
};
},
},
visualization: {
icon: 'visualizeApp',
defaultSearchField: 'title',
getTitle(obj) {
return obj.attributes.title;
},
getEditUrl(obj) {
return `/management/kibana/objects/savedVisualizations/${encodeURIComponent(obj.id)}`;
},
getInAppUrl(obj) {
return {
path: `/app/visualize#/edit/${encodeURIComponent(obj.id)}`,
uiCapabilitiesPath: 'visualize.show',
};
},
},
search: {
icon: 'search',
defaultSearchField: 'title',
getTitle(obj) {
return obj.attributes.title;
},
getEditUrl(obj) {
return `/management/kibana/objects/savedSearches/${encodeURIComponent(obj.id)}`;
},
getInAppUrl(obj) {
return {
path: `/app/discover#//${encodeURIComponent(obj.id)}`,
uiCapabilitiesPath: 'discover.show',
};
},
},
dashboard: {
icon: 'dashboardApp',
defaultSearchField: 'title',
getTitle(obj) {
return obj.attributes.title;
},
getEditUrl(obj) {
return `/management/kibana/objects/savedDashboards/${encodeURIComponent(obj.id)}`;
},
getInAppUrl(obj) {
return {
path: `/app/kibana#/dashboard/${encodeURIComponent(obj.id)}`,
uiCapabilitiesPath: 'dashboard.show',
};
},
},
});
describe('findRelationships', () => {
it('should find relationships for dashboards', async () => {
const type = 'dashboard';
const id = 'foo';
const size = 10;
const savedObjectsClient = {
get: () => ({
attributes: {
panelsJSON: JSON.stringify([
{ panelRefName: 'panel_0' },
{ panelRefName: 'panel_1' },
{ panelRefName: 'panel_2' },
]),
},
references: [
{
name: 'panel_0',
type: 'visualization',
id: '1',
},
{
name: 'panel_1',
type: 'visualization',
id: '2',
},
{
name: 'panel_2',
type: 'visualization',
id: '3',
},
],
}),
bulkGet: () => ({ saved_objects: [] }),
find: () => ({
saved_objects: [
{
id: '1',
type: 'visualization',
attributes: {
title: 'Foo',
},
},
{
id: '2',
type: 'visualization',
attributes: {
title: 'Bar',
},
},
{
id: '3',
type: 'visualization',
attributes: {
title: 'FooBar',
},
},
],
}),
};
const result = await findRelationships(type, id, {
size,
savedObjectsClient,
savedObjectsManagement,
savedObjectTypes: ['dashboard', 'visualization', 'search', 'index-pattern'],
});
expect(result).to.eql([
{
id: '1',
type: 'visualization',
relationship: 'parent',
meta: {
icon: 'visualizeApp',
title: 'Foo',
editUrl: '/management/kibana/objects/savedVisualizations/1',
inAppUrl: {
path: '/app/visualize#/edit/1',
uiCapabilitiesPath: 'visualize.show',
},
},
},
{
id: '2',
type: 'visualization',
relationship: 'parent',
meta: {
icon: 'visualizeApp',
title: 'Bar',
editUrl: '/management/kibana/objects/savedVisualizations/2',
inAppUrl: {
path: '/app/visualize#/edit/2',
uiCapabilitiesPath: 'visualize.show',
},
},
},
{
id: '3',
type: 'visualization',
relationship: 'parent',
meta: {
icon: 'visualizeApp',
title: 'FooBar',
editUrl: '/management/kibana/objects/savedVisualizations/3',
inAppUrl: {
path: '/app/visualize#/edit/3',
uiCapabilitiesPath: 'visualize.show',
},
},
},
]);
});
it('should find relationships for visualizations', async () => {
const type = 'visualization';
const id = 'foo';
const size = 10;
const savedObjectsClient = {
get: () => ({
attributes: {
kibanaSavedObjectMeta: {
searchSourceJSON: JSON.stringify({
indexRefName: 'kibanaSavedObjectMeta.searchSourceJSON.index',
}),
},
},
references: [
{
name: 'kibanaSavedObjectMeta.searchSourceJSON.index',
type: 'index-pattern',
id: '1',
},
],
}),
bulkGet: () => ({
saved_objects: [
{
id: '1',
type: 'index-pattern',
attributes: {
title: 'My Index Pattern',
},
},
],
}),
find: () => ({
saved_objects: [
{
id: '1',
type: 'dashboard',
attributes: {
title: 'My Dashboard',
panelsJSON: JSON.stringify([
{
type: 'visualization',
id,
},
{
type: 'visualization',
id: 'foobar',
},
]),
},
},
{
id: '2',
type: 'dashboard',
attributes: {
title: 'Your Dashboard',
panelsJSON: JSON.stringify([
{
type: 'visualization',
id,
},
{
type: 'visualization',
id: 'foobar',
},
]),
},
},
],
}),
};
const result = await findRelationships(type, id, {
size,
savedObjectsClient,
savedObjectsManagement,
savedObjectTypes: ['dashboard', 'visualization', 'search', 'index-pattern'],
});
expect(result).to.eql([
{
id: '1',
type: 'index-pattern',
relationship: 'child',
meta: {
icon: 'indexPatternApp',
title: 'My Index Pattern',
editUrl: '/management/kibana/indexPatterns/patterns/1',
inAppUrl: {
path: '/app/management/kibana/indexPatterns/patterns/1',
uiCapabilitiesPath: 'management.kibana.index_patterns',
},
},
},
{
id: '1',
type: 'dashboard',
relationship: 'parent',
meta: {
icon: 'dashboardApp',
title: 'My Dashboard',
editUrl: '/management/kibana/objects/savedDashboards/1',
inAppUrl: {
path: '/app/kibana#/dashboard/1',
uiCapabilitiesPath: 'dashboard.show',
},
},
},
{
id: '2',
type: 'dashboard',
relationship: 'parent',
meta: {
icon: 'dashboardApp',
title: 'Your Dashboard',
editUrl: '/management/kibana/objects/savedDashboards/2',
inAppUrl: {
path: '/app/kibana#/dashboard/2',
uiCapabilitiesPath: 'dashboard.show',
},
},
},
]);
});
it('should find relationships for saved searches', async () => {
const type = 'search';
const id = 'foo';
const size = 10;
const savedObjectsClient = {
get: () => ({
id: '1',
type: 'search',
attributes: {
kibanaSavedObjectMeta: {
searchSourceJSON: JSON.stringify({
indexRefName: 'kibanaSavedObjectMeta.searchSourceJSON.index',
}),
},
},
references: [
{
name: 'kibanaSavedObjectMeta.searchSourceJSON.index',
type: 'index-pattern',
id: '1',
},
],
}),
bulkGet: () => ({
saved_objects: [
{
id: '1',
type: 'index-pattern',
attributes: {
title: 'My Index Pattern',
},
},
],
}),
find: () => ({
saved_objects: [
{
id: '1',
type: 'visualization',
attributes: {
title: 'Foo',
},
},
{
id: '2',
type: 'visualization',
attributes: {
title: 'Bar',
},
},
{
id: '3',
type: 'visualization',
attributes: {
title: 'FooBar',
},
},
],
}),
};
const result = await findRelationships(type, id, {
size,
savedObjectsClient,
savedObjectsManagement,
savedObjectTypes: ['dashboard', 'visualization', 'search', 'index-pattern'],
});
expect(result).to.eql([
{
id: '1',
type: 'index-pattern',
relationship: 'child',
meta: {
icon: 'indexPatternApp',
title: 'My Index Pattern',
editUrl: '/management/kibana/indexPatterns/patterns/1',
inAppUrl: {
path: '/app/management/kibana/indexPatterns/patterns/1',
uiCapabilitiesPath: 'management.kibana.index_patterns',
},
},
},
{
id: '1',
type: 'visualization',
relationship: 'parent',
meta: {
icon: 'visualizeApp',
title: 'Foo',
editUrl: '/management/kibana/objects/savedVisualizations/1',
inAppUrl: {
path: '/app/visualize#/edit/1',
uiCapabilitiesPath: 'visualize.show',
},
},
},
{
id: '2',
type: 'visualization',
relationship: 'parent',
meta: {
icon: 'visualizeApp',
title: 'Bar',
editUrl: '/management/kibana/objects/savedVisualizations/2',
inAppUrl: {
path: '/app/visualize#/edit/2',
uiCapabilitiesPath: 'visualize.show',
},
},
},
{
id: '3',
type: 'visualization',
relationship: 'parent',
meta: {
icon: 'visualizeApp',
title: 'FooBar',
editUrl: '/management/kibana/objects/savedVisualizations/3',
inAppUrl: {
path: '/app/visualize#/edit/3',
uiCapabilitiesPath: 'visualize.show',
},
},
},
]);
});
it('should find relationships for index patterns', async () => {
const type = 'index-pattern';
const id = 'foo';
const size = 10;
const savedObjectsClient = {
get: () => ({
id: '1',
type: 'index-pattern',
attributes: {
title: 'My Index Pattern',
},
}),
find: () => ({
saved_objects: [
{
id: '1',
type: 'visualization',
attributes: {
title: 'Foo',
kibanaSavedObjectMeta: {
searchSourceJSON: JSON.stringify({
index: 'foo',
}),
},
},
},
{
id: '2',
type: 'visualization',
attributes: {
title: 'Bar',
kibanaSavedObjectMeta: {
searchSourceJSON: JSON.stringify({
index: 'foo',
}),
},
},
},
{
id: '3',
type: 'visualization',
attributes: {
title: 'FooBar',
kibanaSavedObjectMeta: {
searchSourceJSON: JSON.stringify({
index: 'foo2',
}),
},
},
},
{
id: '1',
type: 'search',
attributes: {
title: 'My Saved Search',
},
},
],
}),
};
const result = await findRelationships(type, id, {
size,
savedObjectsClient,
savedObjectsManagement,
savedObjectTypes: ['dashboard', 'visualization', 'search', 'index-pattern'],
});
expect(result).to.eql([
{
id: '1',
type: 'visualization',
relationship: 'parent',
meta: {
icon: 'visualizeApp',
title: 'Foo',
editUrl: '/management/kibana/objects/savedVisualizations/1',
inAppUrl: {
path: '/app/visualize#/edit/1',
uiCapabilitiesPath: 'visualize.show',
},
},
},
{
id: '2',
type: 'visualization',
relationship: 'parent',
meta: {
icon: 'visualizeApp',
title: 'Bar',
editUrl: '/management/kibana/objects/savedVisualizations/2',
inAppUrl: {
path: '/app/visualize#/edit/2',
uiCapabilitiesPath: 'visualize.show',
},
},
},
{
id: '3',
type: 'visualization',
relationship: 'parent',
meta: {
icon: 'visualizeApp',
title: 'FooBar',
editUrl: '/management/kibana/objects/savedVisualizations/3',
inAppUrl: {
path: '/app/visualize#/edit/3',
uiCapabilitiesPath: 'visualize.show',
},
},
},
{
id: '1',
type: 'search',
relationship: 'parent',
meta: {
icon: 'search',
title: 'My Saved Search',
editUrl: '/management/kibana/objects/savedSearches/1',
inAppUrl: {
path: '/app/discover#//1',
uiCapabilitiesPath: 'discover.show',
},
},
},
]);
});
it('should return an empty object for non related objects', async () => {
const type = 'invalid';
const id = 'foo';
const size = 10;
const savedObjectsClient = {
get: () => ({
id: '1',
type: 'index-pattern',
attributes: {
title: 'My Index Pattern',
},
references: [],
}),
find: () => ({ saved_objects: [] }),
};
const result = await findRelationships(type, id, {
size,
savedObjectsClient,
savedObjectsManagement,
savedObjectTypes: ['dashboard', 'visualization', 'search', 'index-pattern'],
});
expect(result).to.eql({});
});
});

View file

@ -1,87 +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 { importDashboards } from './import_dashboards';
import sinon from 'sinon';
describe('importDashboards(req)', () => {
let req;
let bulkCreateStub;
beforeEach(() => {
bulkCreateStub = sinon.stub().returns(Promise.resolve({ saved_objects: [] }));
req = {
query: {},
payload: {
version: '6.0.0',
objects: [
{ id: 'dashboard-01', type: 'dashboard', attributes: { panelJSON: '{}' } },
{ id: 'panel-01', type: 'visualization', attributes: { visState: '{}' } },
],
},
getSavedObjectsClient() {
return {
bulkCreate: bulkCreateStub,
};
},
};
});
test('should call bulkCreate with each asset', () => {
return importDashboards(req).then(() => {
expect(bulkCreateStub.calledOnce).toEqual(true);
expect(bulkCreateStub.args[0][0]).toEqual([
{
id: 'dashboard-01',
type: 'dashboard',
attributes: { panelJSON: '{}' },
migrationVersion: {},
},
{
id: 'panel-01',
type: 'visualization',
attributes: { visState: '{}' },
migrationVersion: {},
},
]);
});
});
test('should call bulkCreate with overwrite true if force is truthy', () => {
req.query = { force: 'true' };
return importDashboards(req).then(() => {
expect(bulkCreateStub.calledOnce).toEqual(true);
expect(bulkCreateStub.args[0][1]).toEqual({ overwrite: true });
});
});
test('should exclude types based on exclude argument', () => {
req.query = { exclude: 'visualization' };
return importDashboards(req).then(() => {
expect(bulkCreateStub.calledOnce).toEqual(true);
expect(bulkCreateStub.args[0][0]).toEqual([
{
id: 'dashboard-01',
type: 'dashboard',
attributes: { panelJSON: '{}' },
migrationVersion: {},
},
]);
});
});
});

View file

@ -1,148 +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 { injectMetaAttributes } from './inject_meta_attributes';
function getManagementMock(savedObjectSchemas) {
return {
isImportAndExportable(type) {
return (
!savedObjectSchemas[type] || savedObjectSchemas[type].isImportableAndExportable !== false
);
},
getDefaultSearchField(type) {
return savedObjectSchemas[type] && savedObjectSchemas[type].defaultSearchField;
},
getIcon(type) {
return savedObjectSchemas[type] && savedObjectSchemas[type].icon;
},
getTitle(savedObject) {
const { type } = savedObject;
const getTitle = savedObjectSchemas[type] && savedObjectSchemas[type].getTitle;
if (getTitle) {
return getTitle(savedObject);
}
},
getEditUrl(savedObject) {
const { type } = savedObject;
const getEditUrl = savedObjectSchemas[type] && savedObjectSchemas[type].getEditUrl;
if (getEditUrl) {
return getEditUrl(savedObject);
}
},
getInAppUrl(savedObject) {
const { type } = savedObject;
const getInAppUrl = savedObjectSchemas[type] && savedObjectSchemas[type].getInAppUrl;
if (getInAppUrl) {
return getInAppUrl(savedObject);
}
},
};
}
test('works when no schema is defined for the type', () => {
const savedObject = { type: 'a' };
const savedObjectsManagement = getManagementMock({});
const result = injectMetaAttributes(savedObject, savedObjectsManagement);
expect(result).toEqual({ type: 'a', meta: {} });
});
test('inject icon into meta attribute', () => {
const savedObject = {
type: 'a',
};
const savedObjectsManagement = getManagementMock({
a: {
icon: 'my-icon',
},
});
const result = injectMetaAttributes(savedObject, savedObjectsManagement);
expect(result).toEqual({
type: 'a',
meta: {
icon: 'my-icon',
},
});
});
test('injects title into meta attribute', () => {
const savedObject = {
type: 'a',
};
const savedObjectsManagement = getManagementMock({
a: {
getTitle() {
return 'my-title';
},
},
});
const result = injectMetaAttributes(savedObject, savedObjectsManagement);
expect(result).toEqual({
type: 'a',
meta: {
title: 'my-title',
},
});
});
test('injects editUrl into meta attribute', () => {
const savedObject = {
type: 'a',
};
const savedObjectsManagement = getManagementMock({
a: {
getEditUrl() {
return 'my-edit-url';
},
},
});
const result = injectMetaAttributes(savedObject, savedObjectsManagement);
expect(result).toEqual({
type: 'a',
meta: {
editUrl: 'my-edit-url',
},
});
});
test('injects inAppUrl meta attribute', () => {
const savedObject = {
type: 'a',
};
const savedObjectsManagement = getManagementMock({
a: {
getInAppUrl() {
return {
path: 'my-in-app-url',
uiCapabilitiesPath: 'ui.path',
};
},
},
});
const result = injectMetaAttributes(savedObject, savedObjectsManagement);
expect(result).toEqual({
type: 'a',
meta: {
inAppUrl: {
path: 'my-in-app-url',
uiCapabilitiesPath: 'ui.path',
},
},
});
});

View file

@ -1,64 +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 { pick } from 'lodash';
import { injectMetaAttributes } from './inject_meta_attributes';
export async function findRelationships(type, id, options = {}) {
const { size, savedObjectsClient, savedObjectTypes, savedObjectsManagement } = options;
const { references = [] } = await savedObjectsClient.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(({ type, id }) => [`${type}:${id}`, { id, type }])
);
const [referencedObjects, referencedResponse] = await Promise.all([
referencedToBulkGetOpts.size > 0
? savedObjectsClient.bulkGet([...referencedToBulkGetOpts.values()])
: Promise.resolve({ saved_objects: [] }),
savedObjectsClient.find({
hasReference: { type, id },
perPage: size,
type: savedObjectTypes,
}),
]);
return [].concat(
referencedObjects.saved_objects
.map((obj) => injectMetaAttributes(obj, savedObjectsManagement))
.map(extractCommonProperties)
.map((obj) => ({
...obj,
relationship: 'child',
})),
referencedResponse.saved_objects
.map((obj) => injectMetaAttributes(obj, savedObjectsManagement))
.map(extractCommonProperties)
.map((obj) => ({
...obj,
relationship: 'parent',
}))
);
}
function extractCommonProperties(savedObject) {
return pick(savedObject, ['id', 'type', 'meta']);
}

View file

@ -1,52 +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 moment from 'moment';
import { exportDashboards } from '../../../lib/export/export_dashboards';
export function exportApi(server) {
server.route({
path: '/api/kibana/dashboards/export',
config: {
validate: {
query: Joi.object().keys({
dashboard: Joi.alternatives()
.try(Joi.string(), Joi.array().items(Joi.string()))
.required(),
}),
},
tags: ['api'],
},
method: ['GET'],
handler: async (req, h) => {
const currentDate = moment.utc();
return exportDashboards(req).then((resp) => {
const json = JSON.stringify(resp, null, ' ');
const filename = `kibana-dashboards.${currentDate.format('YYYY-MM-DD-HH-mm-ss')}.json`;
return h
.response(json)
.header('Content-Disposition', `attachment; filename="${filename}"`)
.header('Content-Type', 'application/json')
.header('Content-Length', Buffer.byteLength(json, 'utf8'));
});
},
});
}

View file

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

View file

@ -17,17 +17,7 @@
* under the License.
*/
export function injectMetaAttributes(savedObject, savedObjectsManagement) {
const result = {
...savedObject,
meta: savedObject.meta || {},
};
import { PluginInitializer } from 'src/core/server';
import { LegacyExportPlugin } from './plugin';
// 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;
}
export const plugin: PluginInitializer<{}, {}> = (context) => new LegacyExportPlugin(context);

View file

@ -18,8 +18,8 @@
*/
import { SavedObject, SavedObjectAttributes } from 'src/core/server';
import { savedObjectsClientMock } from '../../../../../core/server/mocks';
import { collectReferencesDeep } from './collect_references_deep';
import { savedObjectsClientMock } from '../../../../../../core/server/mocks';
const data: Array<SavedObject<SavedObjectAttributes>> = [
{

View file

@ -29,7 +29,7 @@ interface ObjectsToCollect {
export async function collectReferencesDeep(
savedObjectClient: SavedObjectsClientContract,
objects: ObjectsToCollect[]
) {
): Promise<SavedObject[]> {
let result: SavedObject[] = [];
const queue = [...objects];
while (queue.length !== 0) {

View file

@ -17,19 +17,19 @@
* under the License.
*/
import _ from 'lodash';
import { SavedObjectsClientContract } from 'src/core/server';
import { collectReferencesDeep } from './collect_references_deep';
export async function exportDashboards(req) {
const ids = _.flatten([req.query.dashboard]);
const config = req.server.config();
const savedObjectsClient = req.getSavedObjectsClient();
export async function exportDashboards(
ids: string[],
savedObjectsClient: SavedObjectsClientContract,
kibanaVersion: string
) {
const objectsToExport = ids.map((id) => ({ id, type: 'dashboard' }));
const objects = await collectReferencesDeep(savedObjectsClient, objectsToExport);
return {
version: config.get('pkg.version'),
version: kibanaVersion,
objects,
};
}

View file

@ -0,0 +1,92 @@
/*
* 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 { savedObjectsClientMock } from '../../../../../core/server/mocks';
import { SavedObject } from '../../../../../core/server';
import { importDashboards } from './import_dashboards';
describe('importDashboards(req)', () => {
let savedObjectClient: ReturnType<typeof savedObjectsClientMock.create>;
let importedObjects: SavedObject[];
beforeEach(() => {
savedObjectClient = savedObjectsClientMock.create();
savedObjectClient.bulkCreate.mockResolvedValue({ saved_objects: [] });
importedObjects = [
{ id: 'dashboard-01', type: 'dashboard', attributes: { panelJSON: '{}' }, references: [] },
{ id: 'panel-01', type: 'visualization', attributes: { visState: '{}' }, references: [] },
];
});
test('should call bulkCreate with each asset', async () => {
await importDashboards(savedObjectClient, importedObjects, { overwrite: false, exclude: [] });
expect(savedObjectClient.bulkCreate).toHaveBeenCalledTimes(1);
expect(savedObjectClient.bulkCreate).toHaveBeenCalledWith(
[
{
id: 'dashboard-01',
type: 'dashboard',
attributes: { panelJSON: '{}' },
references: [],
migrationVersion: {},
},
{
id: 'panel-01',
type: 'visualization',
attributes: { visState: '{}' },
references: [],
migrationVersion: {},
},
],
{ overwrite: false }
);
});
test('should call bulkCreate with overwrite true if force is truthy', async () => {
await importDashboards(savedObjectClient, importedObjects, { overwrite: true, exclude: [] });
expect(savedObjectClient.bulkCreate).toHaveBeenCalledTimes(1);
expect(savedObjectClient.bulkCreate).toHaveBeenCalledWith(expect.any(Array), {
overwrite: true,
});
});
test('should exclude types based on exclude argument', async () => {
await importDashboards(savedObjectClient, importedObjects, {
overwrite: false,
exclude: ['visualization'],
});
expect(savedObjectClient.bulkCreate).toHaveBeenCalledTimes(1);
expect(savedObjectClient.bulkCreate).toHaveBeenCalledWith(
[
{
id: 'dashboard-01',
type: 'dashboard',
attributes: { panelJSON: '{}' },
references: [],
migrationVersion: {},
},
],
{ overwrite: false }
);
});
});

View file

@ -17,21 +17,19 @@
* under the License.
*/
import { flatten } from 'lodash';
export async function importDashboards(req) {
const { payload } = req;
const overwrite = 'force' in req.query && req.query.force !== false;
const exclude = flatten([req.query.exclude]);
const savedObjectsClient = req.getSavedObjectsClient();
import { SavedObject, SavedObjectsClientContract } from 'src/core/server';
export async function importDashboards(
savedObjectsClient: SavedObjectsClientContract,
objects: SavedObject[],
{ overwrite, exclude }: { overwrite: boolean; exclude: string[] }
) {
// The server assumes that documents with no migrationVersion are up to date.
// That assumption enables Kibana and other API consumers to not have to build
// up migrationVersion prior to creating new objects. But it means that imports
// need to set migrationVersion to something other than undefined, so that imported
// docs are not seen as automatically up-to-date.
const docs = payload.objects
const docs = objects
.filter((item) => !exclude.includes(item.type))
.map((doc) => ({ ...doc, migrationVersion: doc.migrationVersion || {} }));

View file

@ -0,0 +1,21 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export { exportDashboards } from './export/export_dashboards';
export { importDashboards } from './import/import_dashboards';

View file

@ -17,29 +17,26 @@
* under the License.
*/
import Joi from 'joi';
import { importDashboards } from '../../../lib/import/import_dashboards';
import { Plugin, CoreSetup, PluginInitializerContext } from 'kibana/server';
import { registerRoutes } from './routes';
export function importApi(server) {
server.route({
path: '/api/kibana/dashboards/import',
method: ['POST'],
config: {
validate: {
payload: Joi.object().keys({
objects: Joi.array(),
version: Joi.string(),
}),
query: Joi.object().keys({
force: Joi.boolean().default(false),
exclude: [Joi.string(), Joi.array().items(Joi.string())],
}),
},
tags: ['api'],
},
export class LegacyExportPlugin implements Plugin<{}, {}> {
private readonly kibanaVersion: string;
handler: async (req) => {
return await importDashboards(req);
},
});
constructor(context: PluginInitializerContext) {
this.kibanaVersion = context.env.packageInfo.version;
}
public setup({ http }: CoreSetup) {
const router = http.createRouter();
registerRoutes(router, this.kibanaVersion);
return {};
}
public start() {
return {};
}
public stop() {}
}

View file

@ -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 moment from 'moment';
import { schema } from '@kbn/config-schema';
import { IRouter } from 'src/core/server';
import { exportDashboards } from '../lib';
export const registerExportRoute = (router: IRouter, kibanaVersion: string) => {
router.get(
{
path: '/api/kibana/dashboards/export',
validate: {
query: schema.object({
dashboard: schema.oneOf([schema.string(), schema.arrayOf(schema.string())]),
}),
},
options: {
tags: ['api'],
},
},
async (ctx, req, res) => {
const ids = Array.isArray(req.query.dashboard) ? req.query.dashboard : [req.query.dashboard];
const { client } = ctx.core.savedObjects;
const exported = await exportDashboards(ids, client, kibanaVersion);
const filename = `kibana-dashboards.${moment.utc().format('YYYY-MM-DD-HH-mm-ss')}.json`;
const body = JSON.stringify(exported, null, ' ');
return res.ok({
body,
headers: {
'Content-Disposition': `attachment; filename="${filename}"`,
'Content-Type': 'application/json',
'Content-Length': `${Buffer.byteLength(body, 'utf8')}`,
},
});
}
);
};

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, SavedObject } from 'src/core/server';
import { importDashboards } from '../lib';
export const registerImportRoute = (router: IRouter) => {
router.post(
{
path: '/api/kibana/dashboards/import',
validate: {
body: schema.object({
objects: schema.arrayOf(schema.recordOf(schema.string(), schema.any())),
version: schema.string(),
}),
query: schema.object({
force: schema.boolean({ defaultValue: false }),
exclude: schema.oneOf([schema.string(), schema.arrayOf(schema.string())], {
defaultValue: [],
}),
}),
},
options: {
tags: ['api'],
},
},
async (ctx, req, res) => {
const { client } = ctx.core.savedObjects;
const objects = req.body.objects as SavedObject[];
const { force, exclude } = req.query;
const result = await importDashboards(client, objects, {
overwrite: force,
exclude: Array.isArray(exclude) ? exclude : [exclude],
});
return res.ok({
body: result,
});
}
);
};

View file

@ -0,0 +1,27 @@
/*
* 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 { IRouter } from 'src/core/server';
import { registerImportRoute } from './import';
import { registerExportRoute } from './export';
export const registerRoutes = (router: IRouter, kibanaVersion: string) => {
registerExportRoute(router, kibanaVersion);
registerImportRoute(router);
};