diff --git a/docs/api/saved-objects/bulk_create.asciidoc b/docs/api/saved-objects/bulk_create.asciidoc
index 62b63674e6f81..1e749cb67c472 100644
--- a/docs/api/saved-objects/bulk_create.asciidoc
+++ b/docs/api/saved-objects/bulk_create.asciidoc
@@ -33,6 +33,9 @@ contains the following properties:
`attributes` (required)::
(object) The data to persist
+`references` (optional)::
+ (array) An array of objects with `name`, `id`, and `type` properties that describe the other saved objects this object references. The `name` can be used in the attributes to refer to the other saved object, but never the `id`, which may be updated automatically in the future during migrations or import/export.
+
`version` (optional)::
(number) Enables specifying a version
diff --git a/docs/api/saved-objects/create.asciidoc b/docs/api/saved-objects/create.asciidoc
index 7631dd296c2b3..c4a2cf260f7d9 100644
--- a/docs/api/saved-objects/create.asciidoc
+++ b/docs/api/saved-objects/create.asciidoc
@@ -33,6 +33,8 @@ Note: You cannot access this endpoint via the Console in Kibana.
`attributes` (required)::
(object) The data to persist
+`references` (optional)::
+ (array) An array of objects with `name`, `id`, and `type` properties that describe the other saved objects this object references. The `name` can be used in the attributes to refer to the other saved object, but never the `id`, which may be updated automatically in the future during migrations or import/export.
==== Examples
diff --git a/docs/api/saved-objects/find.asciidoc b/docs/api/saved-objects/find.asciidoc
index 8a866c9de71d7..43fea5b5116c6 100644
--- a/docs/api/saved-objects/find.asciidoc
+++ b/docs/api/saved-objects/find.asciidoc
@@ -29,6 +29,8 @@ Note: You cannot access this endpoint via the Console in Kibana.
(array|string) The fields to return in the response
`sort_field` (optional)::
(string) The field on which the response will be sorted
+`has_reference` (optional)::
+ (object) Filters to objects having a relationship with the type and id combination
[NOTE]
==============================================
diff --git a/docs/api/saved-objects/update.asciidoc b/docs/api/saved-objects/update.asciidoc
index 2c61f2e66b4b0..092128040d3eb 100644
--- a/docs/api/saved-objects/update.asciidoc
+++ b/docs/api/saved-objects/update.asciidoc
@@ -26,6 +26,8 @@ Note: You cannot access this endpoint via the Console in Kibana.
`attributes` (required)::
(object) The data to persist
+`references` (optional)::
+ (array) An array of objects with `name`, `id`, and `type` properties that describe the other saved objects this object references. The `name` can be used in the attributes to refer to the other saved object, but never the `id`, which may be updated automatically in the future during migrations or import/export.
==== Examples
diff --git a/src/legacy/core_plugins/kibana/mappings.json b/src/legacy/core_plugins/kibana/mappings.json
index 155742b15c8a7..243d49c448c6d 100644
--- a/src/legacy/core_plugins/kibana/mappings.json
+++ b/src/legacy/core_plugins/kibana/mappings.json
@@ -42,7 +42,7 @@
}
}
},
- "savedSearchId": {
+ "savedSearchRefName": {
"type": "keyword"
},
"title": {
diff --git a/src/legacy/core_plugins/kibana/migrations.js b/src/legacy/core_plugins/kibana/migrations.js
index 8dee9566f0e8a..1d672936b389f 100644
--- a/src/legacy/core_plugins/kibana/migrations.js
+++ b/src/legacy/core_plugins/kibana/migrations.js
@@ -19,9 +19,53 @@
import { cloneDeep, get, omit } from 'lodash';
+function migrateIndexPattern(doc) {
+ const searchSourceJSON = get(doc, 'attributes.kibanaSavedObjectMeta.searchSourceJSON');
+ if (typeof searchSourceJSON !== 'string') {
+ return;
+ }
+ let searchSource;
+ try {
+ searchSource = JSON.parse(searchSourceJSON);
+ } catch (e) {
+ // Let it go, the data is invalid and we'll leave it as is
+ return;
+ }
+ if (!searchSource.index) {
+ return;
+ }
+ doc.references.push({
+ name: 'kibanaSavedObjectMeta.searchSourceJSON.index',
+ type: 'index-pattern',
+ id: searchSource.index,
+ });
+ searchSource.indexRefName = 'kibanaSavedObjectMeta.searchSourceJSON.index';
+ delete searchSource.index;
+ doc.attributes.kibanaSavedObjectMeta.searchSourceJSON = JSON.stringify(searchSource);
+}
+
export const migrations = {
visualization: {
'7.0.0': (doc) => {
+ // Set new "references" attribute
+ doc.references = doc.references || [];
+
+ // Migrate index pattern
+ migrateIndexPattern(doc);
+
+ // Migrate saved search
+ const savedSearchId = get(doc, 'attributes.savedSearchId');
+ if (savedSearchId) {
+ doc.references.push({
+ type: 'search',
+ name: 'search_0',
+ id: savedSearchId,
+ });
+ doc.attributes.savedSearchRefName = 'search_0';
+ delete doc.attributes.savedSearchId;
+ }
+
+ // Migrate table splits
try {
const visState = JSON.parse(doc.attributes.visState);
if (get(visState, 'type') !== 'table') {
@@ -55,5 +99,52 @@ export const migrations = {
throw new Error(`Failure attempting to migrate saved object '${doc.attributes.title}' - ${e}`);
}
}
- }
+ },
+ dashboard: {
+ '7.0.0': (doc) => {
+ // Set new "references" attribute
+ doc.references = doc.references || [];
+ // Migrate index pattern
+ migrateIndexPattern(doc);
+ // Migrate panels
+ const panelsJSON = get(doc, 'attributes.panelsJSON');
+ if (typeof panelsJSON !== 'string') {
+ return doc;
+ }
+ let panels;
+ try {
+ panels = JSON.parse(panelsJSON);
+ } catch (e) {
+ // Let it go, the data is invalid and we'll leave it as is
+ return doc;
+ }
+ if (!Array.isArray(panels)) {
+ return doc;
+ }
+ panels.forEach((panel, i) => {
+ if (!panel.type || !panel.id) {
+ return;
+ }
+ panel.panelRefName = `panel_${i}`;
+ doc.references.push({
+ name: `panel_${i}`,
+ type: panel.type,
+ id: panel.id,
+ });
+ delete panel.type;
+ delete panel.id;
+ });
+ doc.attributes.panelsJSON = JSON.stringify(panels);
+ return doc;
+ },
+ },
+ search: {
+ '7.0.0': (doc) => {
+ // Set new "references" attribute
+ doc.references = doc.references || [];
+ // Migrate index pattern
+ migrateIndexPattern(doc);
+ return doc;
+ },
+ },
};
diff --git a/src/legacy/core_plugins/kibana/migrations.test.js b/src/legacy/core_plugins/kibana/migrations.test.js
index dc8ddb594af02..cafd141fc2f19 100644
--- a/src/legacy/core_plugins/kibana/migrations.test.js
+++ b/src/legacy/core_plugins/kibana/migrations.test.js
@@ -19,8 +19,7 @@
import { migrations } from './migrations';
-describe('table vis migrations', () => {
-
+describe('visualization', () => {
describe('7.0.0', () => {
const migrate = doc => migrations.visualization['7.0.0'](doc);
const generateDoc = ({ type, aggs }) => ({
@@ -31,9 +30,296 @@ describe('table vis migrations', () => {
uiStateJSON: '{}',
version: 1,
kibanaSavedObjectMeta: {
- searchSourceJSON: '{}'
- }
- }
+ searchSourceJSON: '{}',
+ },
+ },
+ });
+
+ it('does not throw error on empty object', () => {
+ const migratedDoc = migrate({
+ attributes: {
+ visState: '{}',
+ },
+ });
+ expect(migratedDoc).toMatchInlineSnapshot(`
+Object {
+ "attributes": Object {
+ "visState": "{}",
+ },
+ "references": Array [],
+}
+`);
+ });
+
+ it('skips errors when searchSourceJSON is null', () => {
+ const doc = {
+ id: '1',
+ type: 'visualization',
+ attributes: {
+ visState: '{}',
+ kibanaSavedObjectMeta: {
+ searchSourceJSON: null,
+ },
+ savedSearchId: '123',
+ },
+ };
+ const migratedDoc = migrate(doc);
+ expect(migratedDoc).toMatchInlineSnapshot(`
+Object {
+ "attributes": Object {
+ "kibanaSavedObjectMeta": Object {
+ "searchSourceJSON": null,
+ },
+ "savedSearchRefName": "search_0",
+ "visState": "{}",
+ },
+ "id": "1",
+ "references": Array [
+ Object {
+ "id": "123",
+ "name": "search_0",
+ "type": "search",
+ },
+ ],
+ "type": "visualization",
+}
+`);
+ });
+
+ it('skips errors when searchSourceJSON is undefined', () => {
+ const doc = {
+ id: '1',
+ type: 'visualization',
+ attributes: {
+ visState: '{}',
+ kibanaSavedObjectMeta: {
+ searchSourceJSON: undefined,
+ },
+ savedSearchId: '123',
+ },
+ };
+ const migratedDoc = migrate(doc);
+ expect(migratedDoc).toMatchInlineSnapshot(`
+Object {
+ "attributes": Object {
+ "kibanaSavedObjectMeta": Object {
+ "searchSourceJSON": undefined,
+ },
+ "savedSearchRefName": "search_0",
+ "visState": "{}",
+ },
+ "id": "1",
+ "references": Array [
+ Object {
+ "id": "123",
+ "name": "search_0",
+ "type": "search",
+ },
+ ],
+ "type": "visualization",
+}
+`);
+ });
+
+ it('skips error when searchSourceJSON is not a string', () => {
+ const doc = {
+ id: '1',
+ type: 'visualization',
+ attributes: {
+ visState: '{}',
+ kibanaSavedObjectMeta: {
+ searchSourceJSON: 123,
+ },
+ savedSearchId: '123',
+ },
+ };
+ expect(migrate(doc)).toMatchInlineSnapshot(`
+Object {
+ "attributes": Object {
+ "kibanaSavedObjectMeta": Object {
+ "searchSourceJSON": 123,
+ },
+ "savedSearchRefName": "search_0",
+ "visState": "{}",
+ },
+ "id": "1",
+ "references": Array [
+ Object {
+ "id": "123",
+ "name": "search_0",
+ "type": "search",
+ },
+ ],
+ "type": "visualization",
+}
+`);
+ });
+
+ it('skips error when searchSourceJSON is invalid json', () => {
+ const doc = {
+ id: '1',
+ type: 'visualization',
+ attributes: {
+ visState: '{}',
+ kibanaSavedObjectMeta: {
+ searchSourceJSON: '{abc123}',
+ },
+ savedSearchId: '123',
+ },
+ };
+ expect(migrate(doc)).toMatchInlineSnapshot(`
+Object {
+ "attributes": Object {
+ "kibanaSavedObjectMeta": Object {
+ "searchSourceJSON": "{abc123}",
+ },
+ "savedSearchRefName": "search_0",
+ "visState": "{}",
+ },
+ "id": "1",
+ "references": Array [
+ Object {
+ "id": "123",
+ "name": "search_0",
+ "type": "search",
+ },
+ ],
+ "type": "visualization",
+}
+`);
+ });
+
+ it('skips error when "index" is missing from searchSourceJSON', () => {
+ const doc = {
+ id: '1',
+ type: 'visualization',
+ attributes: {
+ visState: '{}',
+ kibanaSavedObjectMeta: {
+ searchSourceJSON: JSON.stringify({ bar: true }),
+ },
+ savedSearchId: '123',
+ },
+ };
+ const migratedDoc = migrate(doc);
+ expect(migratedDoc).toMatchInlineSnapshot(`
+Object {
+ "attributes": Object {
+ "kibanaSavedObjectMeta": Object {
+ "searchSourceJSON": "{\\"bar\\":true}",
+ },
+ "savedSearchRefName": "search_0",
+ "visState": "{}",
+ },
+ "id": "1",
+ "references": Array [
+ Object {
+ "id": "123",
+ "name": "search_0",
+ "type": "search",
+ },
+ ],
+ "type": "visualization",
+}
+`);
+ });
+
+ it('extracts "index" attribute from doc', () => {
+ const doc = {
+ id: '1',
+ type: 'visualization',
+ attributes: {
+ visState: '{}',
+ kibanaSavedObjectMeta: {
+ searchSourceJSON: JSON.stringify({ bar: true, index: 'pattern*' }),
+ },
+ savedSearchId: '123',
+ },
+ };
+ const migratedDoc = migrate(doc);
+ expect(migratedDoc).toMatchInlineSnapshot(`
+Object {
+ "attributes": Object {
+ "kibanaSavedObjectMeta": Object {
+ "searchSourceJSON": "{\\"bar\\":true,\\"indexRefName\\":\\"kibanaSavedObjectMeta.searchSourceJSON.index\\"}",
+ },
+ "savedSearchRefName": "search_0",
+ "visState": "{}",
+ },
+ "id": "1",
+ "references": Array [
+ Object {
+ "id": "pattern*",
+ "name": "kibanaSavedObjectMeta.searchSourceJSON.index",
+ "type": "index-pattern",
+ },
+ Object {
+ "id": "123",
+ "name": "search_0",
+ "type": "search",
+ },
+ ],
+ "type": "visualization",
+}
+`);
+ });
+
+ it('skips extracting savedSearchId when missing', () => {
+ const doc = {
+ id: '1',
+ attributes: {
+ visState: '{}',
+ kibanaSavedObjectMeta: {
+ searchSourceJSON: '{}',
+ },
+ },
+ };
+ const migratedDoc = migrate(doc);
+ expect(migratedDoc).toMatchInlineSnapshot(`
+Object {
+ "attributes": Object {
+ "kibanaSavedObjectMeta": Object {
+ "searchSourceJSON": "{}",
+ },
+ "visState": "{}",
+ },
+ "id": "1",
+ "references": Array [],
+}
+`);
+ });
+
+ it('extract savedSearchId from doc', () => {
+ const doc = {
+ id: '1',
+ attributes: {
+ visState: '{}',
+ kibanaSavedObjectMeta: {
+ searchSourceJSON: '{}',
+ },
+ savedSearchId: '123',
+ },
+ };
+ const migratedDoc = migrate(doc);
+ expect(migratedDoc).toMatchInlineSnapshot(`
+Object {
+ "attributes": Object {
+ "kibanaSavedObjectMeta": Object {
+ "searchSourceJSON": "{}",
+ },
+ "savedSearchRefName": "search_0",
+ "visState": "{}",
+ },
+ "id": "1",
+ "references": Array [
+ Object {
+ "id": "123",
+ "name": "search_0",
+ "type": "search",
+ },
+ ],
+}
+`);
});
it('should return a new object if vis is table and has multiple split aggs', () => {
@@ -41,17 +327,17 @@ describe('table vis migrations', () => {
{
id: '1',
schema: 'metric',
- params: {}
+ params: {},
},
{
id: '2',
schema: 'split',
- params: { foo: 'bar', row: true }
+ params: { foo: 'bar', row: true },
},
{
id: '3',
schema: 'split',
- params: { hey: 'ya', row: false }
+ params: { hey: 'ya', row: false },
},
];
const tableDoc = generateDoc({ type: 'table', aggs });
@@ -73,18 +359,18 @@ describe('table vis migrations', () => {
{
id: '1',
schema: 'metric',
- params: {}
+ params: {},
},
{
id: '2',
schema: 'split',
- params: { foo: 'bar', row: true }
+ params: { foo: 'bar', row: true },
},
{
id: '3',
schema: 'segment',
- params: { hey: 'ya' }
- }
+ params: { hey: 'ya' },
+ },
];
const pieDoc = generateDoc({ type: 'pie', aggs });
const expected = pieDoc;
@@ -97,13 +383,13 @@ describe('table vis migrations', () => {
{
id: '1',
schema: 'metric',
- params: {}
+ params: {},
},
{
id: '2',
schema: 'split',
- params: { foo: 'bar', row: true }
- }
+ params: { foo: 'bar', row: true },
+ },
];
const tableDoc = generateDoc({ type: 'table', aggs });
const expected = tableDoc;
@@ -116,23 +402,23 @@ describe('table vis migrations', () => {
{
id: '1',
schema: 'metric',
- params: {}
+ params: {},
},
{
id: '2',
schema: 'split',
- params: { foo: 'bar', row: true }
+ params: { foo: 'bar', row: true },
},
{
id: '3',
schema: 'split',
- params: { hey: 'ya', row: false }
+ params: { hey: 'ya', row: false },
},
{
id: '4',
schema: 'bucket',
- params: { heyyy: 'yaaa' }
- }
+ params: { heyyy: 'yaaa' },
+ },
];
const expected = ['metric', 'split', 'bucket', 'bucket'];
const migrated = migrate(generateDoc({ type: 'table', aggs }));
@@ -145,18 +431,18 @@ describe('table vis migrations', () => {
{
id: '1',
schema: 'metric',
- params: {}
+ params: {},
},
{
id: '2',
schema: 'split',
- params: { foo: 'bar', row: true }
+ params: { foo: 'bar', row: true },
},
{
id: '3',
schema: 'split',
- params: { hey: 'ya', row: false }
- }
+ params: { hey: 'ya', row: false },
+ },
];
const expected = [{}, { foo: 'bar', row: true }, { hey: 'ya' }];
const migrated = migrate(generateDoc({ type: 'table', aggs }));
@@ -173,12 +459,555 @@ describe('table vis migrations', () => {
uiStateJSON: '{}',
version: 1,
kibanaSavedObjectMeta: {
- searchSourceJSON: '{}'
- }
- }
+ searchSourceJSON: '{}',
+ },
+ },
};
expect(() => migrate(doc)).toThrowError(/My Vis/);
});
});
+});
+
+describe('dashboard', () => {
+ describe('7.0.0', () => {
+ const migration = migrations.dashboard['7.0.0'];
+
+ test('skips error on empty object', () => {
+ expect(migration({})).toMatchInlineSnapshot(`
+Object {
+ "references": Array [],
+}
+`);
+ });
+
+ test('skips errors when searchSourceJSON is null', () => {
+ const doc = {
+ id: '1',
+ type: 'dashboard',
+ attributes: {
+ kibanaSavedObjectMeta: {
+ searchSourceJSON: null,
+ },
+ panelsJSON:
+ '[{"id":"1","type":"visualization","foo":true},{"id":"2","type":"visualization","bar":true}]',
+ },
+ };
+ const migratedDoc = migration(doc);
+ expect(migratedDoc).toMatchInlineSnapshot(`
+Object {
+ "attributes": Object {
+ "kibanaSavedObjectMeta": Object {
+ "searchSourceJSON": null,
+ },
+ "panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]",
+ },
+ "id": "1",
+ "references": Array [
+ Object {
+ "id": "1",
+ "name": "panel_0",
+ "type": "visualization",
+ },
+ Object {
+ "id": "2",
+ "name": "panel_1",
+ "type": "visualization",
+ },
+ ],
+ "type": "dashboard",
+}
+`);
+ });
+
+ test('skips errors when searchSourceJSON is undefined', () => {
+ const doc = {
+ id: '1',
+ type: 'dashboard',
+ attributes: {
+ kibanaSavedObjectMeta: {
+ searchSourceJSON: undefined,
+ },
+ panelsJSON:
+ '[{"id":"1","type":"visualization","foo":true},{"id":"2","type":"visualization","bar":true}]',
+ },
+ };
+ const migratedDoc = migration(doc);
+ expect(migratedDoc).toMatchInlineSnapshot(`
+Object {
+ "attributes": Object {
+ "kibanaSavedObjectMeta": Object {
+ "searchSourceJSON": undefined,
+ },
+ "panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]",
+ },
+ "id": "1",
+ "references": Array [
+ Object {
+ "id": "1",
+ "name": "panel_0",
+ "type": "visualization",
+ },
+ Object {
+ "id": "2",
+ "name": "panel_1",
+ "type": "visualization",
+ },
+ ],
+ "type": "dashboard",
+}
+`);
+ });
+
+ test('skips error when searchSourceJSON is not a string', () => {
+ const doc = {
+ id: '1',
+ type: 'dashboard',
+ attributes: {
+ kibanaSavedObjectMeta: {
+ searchSourceJSON: 123,
+ },
+ panelsJSON:
+ '[{"id":"1","type":"visualization","foo":true},{"id":"2","type":"visualization","bar":true}]',
+ },
+ };
+ expect(migration(doc)).toMatchInlineSnapshot(`
+Object {
+ "attributes": Object {
+ "kibanaSavedObjectMeta": Object {
+ "searchSourceJSON": 123,
+ },
+ "panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]",
+ },
+ "id": "1",
+ "references": Array [
+ Object {
+ "id": "1",
+ "name": "panel_0",
+ "type": "visualization",
+ },
+ Object {
+ "id": "2",
+ "name": "panel_1",
+ "type": "visualization",
+ },
+ ],
+ "type": "dashboard",
+}
+`);
+ });
+
+ test('skips error when searchSourceJSON is invalid json', () => {
+ const doc = {
+ id: '1',
+ type: 'dashboard',
+ attributes: {
+ kibanaSavedObjectMeta: {
+ searchSourceJSON: '{abc123}',
+ },
+ panelsJSON:
+ '[{"id":"1","type":"visualization","foo":true},{"id":"2","type":"visualization","bar":true}]',
+ },
+ };
+ expect(migration(doc)).toMatchInlineSnapshot(`
+Object {
+ "attributes": Object {
+ "kibanaSavedObjectMeta": Object {
+ "searchSourceJSON": "{abc123}",
+ },
+ "panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]",
+ },
+ "id": "1",
+ "references": Array [
+ Object {
+ "id": "1",
+ "name": "panel_0",
+ "type": "visualization",
+ },
+ Object {
+ "id": "2",
+ "name": "panel_1",
+ "type": "visualization",
+ },
+ ],
+ "type": "dashboard",
+}
+`);
+ });
+
+ test('skips error when "index" is missing from searchSourceJSON', () => {
+ const doc = {
+ id: '1',
+ type: 'dashboard',
+ attributes: {
+ kibanaSavedObjectMeta: {
+ searchSourceJSON: JSON.stringify({ bar: true }),
+ },
+ panelsJSON:
+ '[{"id":"1","type":"visualization","foo":true},{"id":"2","type":"visualization","bar":true}]',
+ },
+ };
+ const migratedDoc = migration(doc);
+ expect(migratedDoc).toMatchInlineSnapshot(`
+Object {
+ "attributes": Object {
+ "kibanaSavedObjectMeta": Object {
+ "searchSourceJSON": "{\\"bar\\":true}",
+ },
+ "panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]",
+ },
+ "id": "1",
+ "references": Array [
+ Object {
+ "id": "1",
+ "name": "panel_0",
+ "type": "visualization",
+ },
+ Object {
+ "id": "2",
+ "name": "panel_1",
+ "type": "visualization",
+ },
+ ],
+ "type": "dashboard",
+}
+`);
+ });
+
+ test('extracts "index" attribute from doc', () => {
+ const doc = {
+ id: '1',
+ type: 'dashboard',
+ attributes: {
+ kibanaSavedObjectMeta: {
+ searchSourceJSON: JSON.stringify({ bar: true, index: 'pattern*' }),
+ },
+ panelsJSON:
+ '[{"id":"1","type":"visualization","foo":true},{"id":"2","type":"visualization","bar":true}]',
+ },
+ };
+ const migratedDoc = migration(doc);
+ expect(migratedDoc).toMatchInlineSnapshot(`
+Object {
+ "attributes": Object {
+ "kibanaSavedObjectMeta": Object {
+ "searchSourceJSON": "{\\"bar\\":true,\\"indexRefName\\":\\"kibanaSavedObjectMeta.searchSourceJSON.index\\"}",
+ },
+ "panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]",
+ },
+ "id": "1",
+ "references": Array [
+ Object {
+ "id": "pattern*",
+ "name": "kibanaSavedObjectMeta.searchSourceJSON.index",
+ "type": "index-pattern",
+ },
+ Object {
+ "id": "1",
+ "name": "panel_0",
+ "type": "visualization",
+ },
+ Object {
+ "id": "2",
+ "name": "panel_1",
+ "type": "visualization",
+ },
+ ],
+ "type": "dashboard",
+}
+`);
+ });
+
+ test('skips error when panelsJSON is not a string', () => {
+ const doc = {
+ id: '1',
+ attributes: {
+ panelsJSON: 123,
+ },
+ };
+ expect(migration(doc)).toMatchInlineSnapshot(`
+Object {
+ "attributes": Object {
+ "panelsJSON": 123,
+ },
+ "id": "1",
+ "references": Array [],
+}
+`);
+ });
+
+ test('skips error when panelsJSON is not valid JSON', () => {
+ const doc = {
+ id: '1',
+ attributes: {
+ panelsJSON: '{123abc}',
+ },
+ };
+ expect(migration(doc)).toMatchInlineSnapshot(`
+Object {
+ "attributes": Object {
+ "panelsJSON": "{123abc}",
+ },
+ "id": "1",
+ "references": Array [],
+}
+`);
+ });
+
+ test('skips panelsJSON when its not an array', () => {
+ const doc = {
+ id: '1',
+ attributes: {
+ panelsJSON: '{}',
+ },
+ };
+ expect(migration(doc)).toMatchInlineSnapshot(`
+Object {
+ "attributes": Object {
+ "panelsJSON": "{}",
+ },
+ "id": "1",
+ "references": Array [],
+}
+`);
+ });
+
+ test('skips error when a panel is missing "type" attribute', () => {
+ const doc = {
+ id: '1',
+ attributes: {
+ panelsJSON: '[{"id":"123"}]',
+ },
+ };
+ expect(migration(doc)).toMatchInlineSnapshot(`
+Object {
+ "attributes": Object {
+ "panelsJSON": "[{\\"id\\":\\"123\\"}]",
+ },
+ "id": "1",
+ "references": Array [],
+}
+`);
+ });
+
+ test('skips error when a panel is missing "id" attribute', () => {
+ const doc = {
+ id: '1',
+ attributes: {
+ panelsJSON: '[{"type":"visualization"}]',
+ },
+ };
+ expect(migration(doc)).toMatchInlineSnapshot(`
+Object {
+ "attributes": Object {
+ "panelsJSON": "[{\\"type\\":\\"visualization\\"}]",
+ },
+ "id": "1",
+ "references": Array [],
+}
+`);
+ });
+
+ test('extract panel references from doc', () => {
+ const doc = {
+ id: '1',
+ attributes: {
+ panelsJSON:
+ '[{"id":"1","type":"visualization","foo":true},{"id":"2","type":"visualization","bar":true}]',
+ },
+ };
+ const migratedDoc = migration(doc);
+ expect(migratedDoc).toMatchInlineSnapshot(`
+Object {
+ "attributes": Object {
+ "panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]",
+ },
+ "id": "1",
+ "references": Array [
+ Object {
+ "id": "1",
+ "name": "panel_0",
+ "type": "visualization",
+ },
+ Object {
+ "id": "2",
+ "name": "panel_1",
+ "type": "visualization",
+ },
+ ],
+}
+`);
+ });
+ });
+});
+
+describe('search', () => {
+ describe('7.0.0', () => {
+ const migration = migrations.search['7.0.0'];
+ test('skips errors when searchSourceJSON is null', () => {
+ const doc = {
+ id: '123',
+ type: 'search',
+ attributes: {
+ foo: true,
+ kibanaSavedObjectMeta: {
+ searchSourceJSON: null,
+ },
+ },
+ };
+ const migratedDoc = migration(doc);
+ expect(migratedDoc).toMatchInlineSnapshot(`
+Object {
+ "attributes": Object {
+ "foo": true,
+ "kibanaSavedObjectMeta": Object {
+ "searchSourceJSON": null,
+ },
+ },
+ "id": "123",
+ "references": Array [],
+ "type": "search",
+}
+`);
+ });
+
+ test('skips errors when searchSourceJSON is undefined', () => {
+ const doc = {
+ id: '123',
+ type: 'search',
+ attributes: {
+ foo: true,
+ kibanaSavedObjectMeta: {
+ searchSourceJSON: undefined,
+ },
+ },
+ };
+ const migratedDoc = migration(doc);
+ expect(migratedDoc).toMatchInlineSnapshot(`
+Object {
+ "attributes": Object {
+ "foo": true,
+ "kibanaSavedObjectMeta": Object {
+ "searchSourceJSON": undefined,
+ },
+ },
+ "id": "123",
+ "references": Array [],
+ "type": "search",
+}
+`);
+ });
+
+ test('skips error when searchSourceJSON is not a string', () => {
+ const doc = {
+ id: '123',
+ type: 'search',
+ attributes: {
+ foo: true,
+ kibanaSavedObjectMeta: {
+ searchSourceJSON: 123,
+ },
+ },
+ };
+ expect(migration(doc)).toMatchInlineSnapshot(`
+Object {
+ "attributes": Object {
+ "foo": true,
+ "kibanaSavedObjectMeta": Object {
+ "searchSourceJSON": 123,
+ },
+ },
+ "id": "123",
+ "references": Array [],
+ "type": "search",
+}
+`);
+ });
+
+ test('skips error when searchSourceJSON is invalid json', () => {
+ const doc = {
+ id: '123',
+ type: 'search',
+ attributes: {
+ foo: true,
+ kibanaSavedObjectMeta: {
+ searchSourceJSON: '{abc123}',
+ },
+ },
+ };
+ expect(migration(doc)).toMatchInlineSnapshot(`
+Object {
+ "attributes": Object {
+ "foo": true,
+ "kibanaSavedObjectMeta": Object {
+ "searchSourceJSON": "{abc123}",
+ },
+ },
+ "id": "123",
+ "references": Array [],
+ "type": "search",
+}
+`);
+ });
+
+ test('skips error when "index" is missing from searchSourceJSON', () => {
+ const doc = {
+ id: '123',
+ type: 'search',
+ attributes: {
+ foo: true,
+ kibanaSavedObjectMeta: {
+ searchSourceJSON: JSON.stringify({ bar: true }),
+ },
+ },
+ };
+ const migratedDoc = migration(doc);
+ expect(migratedDoc).toMatchInlineSnapshot(`
+Object {
+ "attributes": Object {
+ "foo": true,
+ "kibanaSavedObjectMeta": Object {
+ "searchSourceJSON": "{\\"bar\\":true}",
+ },
+ },
+ "id": "123",
+ "references": Array [],
+ "type": "search",
+}
+`);
+ });
+
+ test('extracts "index" attribute from doc', () => {
+ const doc = {
+ id: '123',
+ type: 'search',
+ attributes: {
+ foo: true,
+ kibanaSavedObjectMeta: {
+ searchSourceJSON: JSON.stringify({ bar: true, index: 'pattern*' }),
+ },
+ },
+ };
+ const migratedDoc = migration(doc);
+ expect(migratedDoc).toMatchInlineSnapshot(`
+Object {
+ "attributes": Object {
+ "foo": true,
+ "kibanaSavedObjectMeta": Object {
+ "searchSourceJSON": "{\\"bar\\":true,\\"indexRefName\\":\\"kibanaSavedObjectMeta.searchSourceJSON.index\\"}",
+ },
+ },
+ "id": "123",
+ "references": Array [
+ Object {
+ "id": "pattern*",
+ "name": "kibanaSavedObjectMeta.searchSourceJSON.index",
+ "type": "index-pattern",
+ },
+ ],
+ "type": "search",
+}
+`);
+ });
+ });
});
diff --git a/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard.js b/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard.js
index 82cf20d9c25af..c3e4b2c668656 100644
--- a/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard.js
+++ b/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard.js
@@ -22,6 +22,10 @@ import { uiModules } from 'ui/modules';
import { createDashboardEditUrl } from '../dashboard_constants';
import { createLegacyClass } from 'ui/utils/legacy_class';
import { SavedObjectProvider } from 'ui/courier';
+import {
+ extractReferences,
+ injectReferences,
+} from './saved_dashboard_references';
const module = uiModules.get('app/dashboard');
@@ -37,6 +41,8 @@ module.factory('SavedDashboard', function (Private, config, i18n) {
type: SavedDashboard.type,
mapping: SavedDashboard.mapping,
searchSource: SavedDashboard.searchsource,
+ extractReferences: extractReferences,
+ injectReferences: injectReferences,
// if this is null/undefined then the SavedObject will be assigned the defaults
id: id,
diff --git a/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard_references.js b/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard_references.js
new file mode 100644
index 0000000000000..724847337a26e
--- /dev/null
+++ b/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard_references.js
@@ -0,0 +1,78 @@
+/*
+ * 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 function extractReferences({ attributes, references = [] }) {
+ const panelReferences = [];
+ const panels = JSON.parse(attributes.panelsJSON);
+ panels.forEach((panel, i) => {
+ if (!panel.type) {
+ throw new Error(`"type" attribute is missing from panel "${i}"`);
+ }
+ if (!panel.id) {
+ throw new Error(`"id" attribute is missing from panel "${i}"`);
+ }
+ panel.panelRefName = `panel_${i}`;
+ panelReferences.push({
+ name: `panel_${i}`,
+ type: panel.type,
+ id: panel.id,
+ });
+ delete panel.type;
+ delete panel.id;
+ });
+ return {
+ references: [
+ ...references,
+ ...panelReferences,
+ ],
+ attributes: {
+ ...attributes,
+ panelsJSON: JSON.stringify(panels),
+ },
+ };
+}
+
+export function injectReferences(savedObject, references) {
+ // Skip if panelsJSON is missing otherwise this will cause saved object import to fail when
+ // importing objects without panelsJSON. At development time of this, there is no guarantee each saved
+ // object has panelsJSON in all previous versions of kibana.
+ if (typeof savedObject.panelsJSON !== 'string') {
+ return;
+ }
+ const panels = JSON.parse(savedObject.panelsJSON);
+ // Same here, prevent failing saved object import if ever panels aren't an array.
+ if (!Array.isArray(panels)) {
+ return;
+ }
+ panels.forEach((panel) => {
+ if (!panel.panelRefName) {
+ return;
+ }
+ const reference = references.find(reference => reference.name === panel.panelRefName);
+ if (!reference) {
+ // Throw an error since "panelRefName" means the reference exists within
+ // "references" and in this scenario we have bad data.
+ throw new Error(`Could not find reference "${panel.panelRefName}"`);
+ }
+ panel.id = reference.id;
+ panel.type = reference.type;
+ delete panel.panelRefName;
+ });
+ savedObject.panelsJSON = JSON.stringify(panels);
+}
diff --git a/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard_references.test.js b/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard_references.test.js
new file mode 100644
index 0000000000000..f32effd667846
--- /dev/null
+++ b/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard_references.test.js
@@ -0,0 +1,220 @@
+/*
+ * 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 { extractReferences, injectReferences } from './saved_dashboard_references';
+
+describe('extractReferences', () => {
+ test('extracts references from panelsJSON', () => {
+ const doc = {
+ id: '1',
+ attributes: {
+ foo: true,
+ panelsJSON: JSON.stringify([
+ {
+ type: 'visualization',
+ id: '1',
+ title: 'Title 1',
+ },
+ {
+ type: 'visualization',
+ id: '2',
+ title: 'Title 2',
+ },
+ ]),
+ },
+ };
+ const updatedDoc = extractReferences(doc);
+ /* eslint-disable max-len */
+ expect(updatedDoc).toMatchInlineSnapshot(`
+Object {
+ "attributes": Object {
+ "foo": true,
+ "panelsJSON": "[{\\"title\\":\\"Title 1\\",\\"panelRefName\\":\\"panel_0\\"},{\\"title\\":\\"Title 2\\",\\"panelRefName\\":\\"panel_1\\"}]",
+ },
+ "references": Array [
+ Object {
+ "id": "1",
+ "name": "panel_0",
+ "type": "visualization",
+ },
+ Object {
+ "id": "2",
+ "name": "panel_1",
+ "type": "visualization",
+ },
+ ],
+}
+`);
+ /* eslint-enable max-len */
+ });
+
+ test('fails when "type" attribute is missing from a panel', () => {
+ const doc = {
+ id: '1',
+ attributes: {
+ foo: true,
+ panelsJSON: JSON.stringify([
+ {
+ id: '1',
+ title: 'Title 1',
+ },
+ ]),
+ },
+ };
+ expect(() => extractReferences(doc)).toThrowErrorMatchingInlineSnapshot(
+ `"\\"type\\" attribute is missing from panel \\"0\\""`
+ );
+ });
+
+ test('fails when "id" attribute is missing from a panel', () => {
+ const doc = {
+ id: '1',
+ attributes: {
+ foo: true,
+ panelsJSON: JSON.stringify([
+ {
+ type: 'visualization',
+ title: 'Title 1',
+ },
+ ]),
+ },
+ };
+ expect(() => extractReferences(doc)).toThrowErrorMatchingInlineSnapshot(
+ `"\\"id\\" attribute is missing from panel \\"0\\""`
+ );
+ });
+});
+
+describe('injectReferences', () => {
+ test('injects references into context', () => {
+ const context = {
+ id: '1',
+ foo: true,
+ panelsJSON: JSON.stringify([
+ {
+ panelRefName: 'panel_0',
+ title: 'Title 1',
+ },
+ {
+ panelRefName: 'panel_1',
+ title: 'Title 2',
+ },
+ ]),
+ };
+ const references = [
+ {
+ name: 'panel_0',
+ type: 'visualization',
+ id: '1',
+ },
+ {
+ name: 'panel_1',
+ type: 'visualization',
+ id: '2',
+ },
+ ];
+ injectReferences(context, references);
+ /* eslint-disable max-len */
+ expect(context).toMatchInlineSnapshot(`
+Object {
+ "foo": true,
+ "id": "1",
+ "panelsJSON": "[{\\"title\\":\\"Title 1\\",\\"id\\":\\"1\\",\\"type\\":\\"visualization\\"},{\\"title\\":\\"Title 2\\",\\"id\\":\\"2\\",\\"type\\":\\"visualization\\"}]",
+}
+`);
+ /* eslint-enable max-len */
+ });
+
+ test('skips when panelsJSON is missing', () => {
+ const context = {
+ id: '1',
+ foo: true,
+ };
+ injectReferences(context, []);
+ expect(context).toMatchInlineSnapshot(`
+Object {
+ "foo": true,
+ "id": "1",
+}
+`);
+ });
+
+ test('skips when panelsJSON is not an array', () => {
+ const context = {
+ id: '1',
+ foo: true,
+ panelsJSON: '{}',
+ };
+ injectReferences(context, []);
+ expect(context).toMatchInlineSnapshot(`
+Object {
+ "foo": true,
+ "id": "1",
+ "panelsJSON": "{}",
+}
+`);
+ });
+
+ test('skips a panel when panelRefName is missing', () => {
+ const context = {
+ id: '1',
+ foo: true,
+ panelsJSON: JSON.stringify([
+ {
+ panelRefName: 'panel_0',
+ title: 'Title 1',
+ },
+ {
+ title: 'Title 2',
+ },
+ ]),
+ };
+ const references = [
+ {
+ name: 'panel_0',
+ type: 'visualization',
+ id: '1',
+ },
+ ];
+ injectReferences(context, references);
+ expect(context).toMatchInlineSnapshot(`
+Object {
+ "foo": true,
+ "id": "1",
+ "panelsJSON": "[{\\"title\\":\\"Title 1\\",\\"id\\":\\"1\\",\\"type\\":\\"visualization\\"},{\\"title\\":\\"Title 2\\"}]",
+}
+`);
+ });
+
+ test(`fails when it can't find the reference in the array`, () => {
+ const context = {
+ id: '1',
+ foo: true,
+ panelsJSON: JSON.stringify([
+ {
+ panelRefName: 'panel_0',
+ title: 'Title 1',
+ },
+ ]),
+ };
+ expect(() => injectReferences(context, [])).toThrowErrorMatchingInlineSnapshot(
+ `"Could not find reference \\"panel_0\\""`
+ );
+ });
+});
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/_view.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/_view.js
index 735d1f22daedf..6f513b46f18ec 100644
--- a/src/legacy/core_plugins/kibana/public/management/sections/objects/_view.js
+++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/_view.js
@@ -125,6 +125,14 @@ uiModules.get('apps/management')
value: '{}'
});
}
+
+ if (!fieldMap.references) {
+ fields.push({
+ name: 'references',
+ type: 'array',
+ value: '[]',
+ });
+ }
};
$scope.notFound = $routeParams.notFound;
@@ -136,7 +144,10 @@ uiModules.get('apps/management')
$scope.obj = obj;
$scope.link = service.urlFor(obj.id);
- const fields = _.reduce(obj.attributes, createField, []);
+ const fields = _.reduce(obj.attributes, createField, []);
+ // Special handling for references which isn't within "attributes"
+ createField(fields, obj.references, 'references');
+
if (service.Class) readObjectClass(fields, service.Class);
// sorts twice since we want numerical sort to prioritize over name,
@@ -234,7 +245,9 @@ uiModules.get('apps/management')
_.set(source, field.name, value);
});
- savedObjectsClient.update(service.type, $routeParams.id, source)
+ const { references, ...attributes } = source;
+
+ savedObjectsClient.update(service.type, $routeParams.id, attributes, { references })
.then(function () {
return redirectHandler('updated');
})
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/relationships/__jest__/relationships.test.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/relationships/__jest__/relationships.test.js
index 9eb269ea46440..fabbfce0395e3 100644
--- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/relationships/__jest__/relationships.test.js
+++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/relationships/__jest__/relationships.test.js
@@ -83,12 +83,12 @@ describe('Relationships', () => {
it('should render searches normally', async () => {
const props = {
getRelationships: jest.fn().mockImplementation(() => ({
- indexPatterns: [
+ 'index-pattern': [
{
id: '1',
}
],
- visualizations: [
+ visualization: [
{
id: '2',
}
@@ -123,7 +123,7 @@ describe('Relationships', () => {
it('should render visualizations normally', async () => {
const props = {
getRelationships: jest.fn().mockImplementation(() => ({
- dashboards: [
+ dashboard: [
{
id: '1',
},
@@ -161,7 +161,7 @@ describe('Relationships', () => {
it('should render dashboards normally', async () => {
const props = {
getRelationships: jest.fn().mockImplementation(() => ({
- visualizations: [
+ visualization: [
{
id: '1',
},
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/relationships/relationships.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/relationships/relationships.js
index c73f77d2687c9..b7491583edf61 100644
--- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/relationships/relationships.js
+++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/relationships/relationships.js
@@ -148,7 +148,7 @@ class RelationshipsUI extends Component {
/>);
break;
case 'search':
- if (type === 'visualizations') {
+ if (type === 'visualization') {
calloutText = ();
+ if (type === 'index-pattern') {
+ calloutColor = 'success';
+ calloutTitle = ();
+ calloutText = ();
+ } else if (type === 'search') {
+ calloutColor = 'success';
+ calloutTitle = ();
+ calloutText = ();
+ } else {
+ calloutText = ();
+ }
break;
case 'index-pattern':
- if (type === 'visualizations') {
+ if (type === 'visualization') {
calloutText = ();
- } else if (type === 'searches') {
+ } else if (type === 'search') {
calloutText = ( reference.name === savedObject.savedSearchRefName);
+ if (!reference) {
+ throw new Error(`Could not find reference "${savedObject.savedSearchRefName}"`);
+ }
+ savedObject.savedSearchId = reference.id;
+ delete savedObject.savedSearchRefName;
+}
diff --git a/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/saved_visualization_references.test.js b/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/saved_visualization_references.test.js
new file mode 100644
index 0000000000000..be9375dc33e56
--- /dev/null
+++ b/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/saved_visualization_references.test.js
@@ -0,0 +1,117 @@
+/*
+ * 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 { extractReferences, injectReferences } from './saved_visualization_references';
+
+describe('extractReferences', () => {
+ test('extracts nothing if savedSearchId is empty', () => {
+ const doc = {
+ id: '1',
+ attributes: {
+ foo: true,
+ },
+ };
+ const updatedDoc = extractReferences(doc);
+ expect(updatedDoc).toMatchInlineSnapshot(`
+Object {
+ "attributes": Object {
+ "foo": true,
+ },
+ "references": Array [],
+}
+`);
+ });
+
+ test('extracts references from savedSearchId', () => {
+ const doc = {
+ id: '1',
+ attributes: {
+ foo: true,
+ savedSearchId: '123',
+ },
+ };
+ const updatedDoc = extractReferences(doc);
+ expect(updatedDoc).toMatchInlineSnapshot(`
+Object {
+ "attributes": Object {
+ "foo": true,
+ "savedSearchId": undefined,
+ "savedSearchRefName": "search_0",
+ },
+ "references": Array [
+ Object {
+ "id": "123",
+ "name": "search_0",
+ "type": "search",
+ },
+ ],
+}
+`);
+ });
+});
+
+describe('injectReferences', () => {
+ test('injects nothing when savedSearchRefName is null', () => {
+ const context = {
+ id: '1',
+ foo: true,
+ };
+ injectReferences(context, []);
+ expect(context).toMatchInlineSnapshot(`
+Object {
+ "foo": true,
+ "id": "1",
+}
+`);
+ });
+
+ test('injects references into context', () => {
+ const context = {
+ id: '1',
+ foo: true,
+ savedSearchRefName: 'search_0',
+ };
+ const references = [
+ {
+ name: 'search_0',
+ type: 'search',
+ id: '123',
+ },
+ ];
+ injectReferences(context, references);
+ expect(context).toMatchInlineSnapshot(`
+Object {
+ "foo": true,
+ "id": "1",
+ "savedSearchId": "123",
+}
+`);
+ });
+
+ test(`fails when it can't find the reference in the array`, () => {
+ const context = {
+ id: '1',
+ foo: true,
+ savedSearchRefName: 'search_0',
+ };
+ expect(() => injectReferences(context, [])).toThrowErrorMatchingInlineSnapshot(
+ `"Could not find reference \\"search_0\\""`
+ );
+ });
+});
diff --git a/src/legacy/core_plugins/kibana/server/lib/__tests__/relationships.js b/src/legacy/core_plugins/kibana/server/lib/__tests__/relationships.js
index 1c5473fe5e311..42845b4cf0eb1 100644
--- a/src/legacy/core_plugins/kibana/server/lib/__tests__/relationships.js
+++ b/src/legacy/core_plugins/kibana/server/lib/__tests__/relationships.js
@@ -27,28 +27,44 @@ describe('findRelationships', () => {
const size = 10;
const savedObjectsClient = {
- _index: '.kibana',
get: () => ({
attributes: {
- panelsJSON: JSON.stringify([{ id: '1' }, { id: '2' }, { id: '3' }]),
+ 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: () => ({
+ 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',
},
@@ -59,11 +75,14 @@ describe('findRelationships', () => {
const result = await findRelationships(
type,
id,
- size,
- savedObjectsClient
+ {
+ size,
+ savedObjectsClient,
+ savedObjectTypes: ['dashboard', 'visualization', 'search', 'index-pattern'],
+ },
);
expect(result).to.eql({
- visualizations: [
+ visualization: [
{ id: '1', title: 'Foo' },
{ id: '2', title: 'Bar' },
{ id: '3', title: 'FooBar' },
@@ -77,11 +96,36 @@ describe('findRelationships', () => {
const size = 10;
const savedObjectsClient = {
- get: () => {},
+ 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([
@@ -98,6 +142,7 @@ describe('findRelationships', () => {
},
{
id: '2',
+ type: 'dashboard',
attributes: {
title: 'Your Dashboard',
panelsJSON: JSON.stringify([
@@ -119,11 +164,17 @@ describe('findRelationships', () => {
const result = await findRelationships(
type,
id,
- size,
- savedObjectsClient
+ {
+ size,
+ savedObjectsClient,
+ savedObjectTypes: ['dashboard', 'visualization', 'search', 'index-pattern'],
+ },
);
expect(result).to.eql({
- dashboards: [
+ 'index-pattern': [
+ { id: '1', title: 'My Index Pattern' },
+ ],
+ dashboard: [
{ id: '1', title: 'My Dashboard' },
{ id: '2', title: 'Your Dashboard' },
],
@@ -136,43 +187,52 @@ describe('findRelationships', () => {
const size = 10;
const savedObjectsClient = {
- get: type => {
- if (type === 'search') {
- return {
+ 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: {
- kibanaSavedObjectMeta: {
- searchSourceJSON: JSON.stringify({
- index: 'index-pattern:1',
- }),
- },
+ title: 'My Index Pattern',
},
- };
- }
-
- return {
- id: 'index-pattern:1',
- 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',
},
@@ -184,16 +244,19 @@ describe('findRelationships', () => {
const result = await findRelationships(
type,
id,
- size,
- savedObjectsClient
+ {
+ size,
+ savedObjectsClient,
+ savedObjectTypes: ['dashboard', 'visualization', 'search', 'index-pattern'],
+ },
);
expect(result).to.eql({
- visualizations: [
+ visualization: [
{ id: '1', title: 'Foo' },
{ id: '2', title: 'Bar' },
{ id: '3', title: 'FooBar' },
],
- indexPatterns: [{ id: 'index-pattern:1', title: 'My Index Pattern' }],
+ 'index-pattern': [{ id: '1', title: 'My Index Pattern' }],
});
});
@@ -203,106 +266,103 @@ describe('findRelationships', () => {
const size = 10;
const savedObjectsClient = {
- get: () => {},
- find: options => {
- if (options.type === 'visualization') {
- return {
- saved_objects: [
- {
- id: '1',
- found: true,
- attributes: {
- title: 'Foo',
- kibanaSavedObjectMeta: {
- searchSourceJSON: JSON.stringify({
- index: 'foo',
- }),
- },
- },
- },
- {
- id: '2',
- found: true,
- attributes: {
- title: 'Bar',
- kibanaSavedObjectMeta: {
- searchSourceJSON: JSON.stringify({
- index: 'foo',
- }),
- },
- },
- },
- {
- id: '3',
- found: true,
- attributes: {
- title: 'FooBar',
- kibanaSavedObjectMeta: {
- searchSourceJSON: JSON.stringify({
- index: 'foo2',
- }),
- },
- },
- },
- ]
- };
- }
-
- return {
- saved_objects: [
- {
- id: '1',
- attributes: {
- title: 'Foo',
- kibanaSavedObjectMeta: {
- searchSourceJSON: JSON.stringify({
- index: 'foo',
- }),
- },
+ 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',
- attributes: {
- title: 'Bar',
- kibanaSavedObjectMeta: {
- searchSourceJSON: JSON.stringify({
- index: 'foo',
- }),
- },
+ },
+ {
+ id: '2',
+ type: 'visualization',
+ attributes: {
+ title: 'Bar',
+ kibanaSavedObjectMeta: {
+ searchSourceJSON: JSON.stringify({
+ index: 'foo',
+ }),
},
},
- {
- id: '3',
- attributes: {
- title: 'FooBar',
- kibanaSavedObjectMeta: {
- searchSourceJSON: JSON.stringify({
- index: 'foo2',
- }),
- },
+ },
+ {
+ 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
+ {
+ size,
+ savedObjectsClient,
+ savedObjectTypes: ['dashboard', 'visualization', 'search', 'index-pattern'],
+ },
);
expect(result).to.eql({
- visualizations: [{ id: '1', title: 'Foo' }, { id: '2', title: 'Bar' }],
- searches: [{ id: '1', title: 'Foo' }, { id: '2', title: 'Bar' }],
+ visualization: [{ id: '1', title: 'Foo' }, { id: '2', title: 'Bar' }, { id: '3', title: 'FooBar' }],
+ search: [{ id: '1', title: 'My Saved Search' }],
});
});
- it('should return an empty object for invalid types', async () => {
+ it('should return an empty object for non related objects', async () => {
const type = 'invalid';
- const result = await findRelationships(type);
+ 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,
+ savedObjectTypes: ['dashboard', 'visualization', 'search', 'index-pattern'],
+ },
+ );
expect(result).to.eql({});
});
});
diff --git a/src/legacy/core_plugins/kibana/server/lib/export/__tests__/collect_dashboards.js b/src/legacy/core_plugins/kibana/server/lib/export/__tests__/collect_dashboards.js
deleted file mode 100644
index fbbe0cc6e3701..0000000000000
--- a/src/legacy/core_plugins/kibana/server/lib/export/__tests__/collect_dashboards.js
+++ /dev/null
@@ -1,89 +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 sinon from 'sinon';
-import * as deps from '../collect_panels';
-import { collectDashboards } from '../collect_dashboards';
-import { expect } from 'chai';
-
-describe('collectDashboards(req, ids)', () => {
-
- let collectPanelsStub;
- const savedObjectsClient = { bulkGet: sinon.stub() };
-
- const ids = ['dashboard-01', 'dashboard-02'];
-
- beforeEach(() => {
- collectPanelsStub = sinon.stub(deps, 'collectPanels');
- collectPanelsStub.onFirstCall().returns(Promise.resolve([
- { id: 'dashboard-01' },
- { id: 'panel-01' },
- { id: 'index-*' }
- ]));
- collectPanelsStub.onSecondCall().returns(Promise.resolve([
- { id: 'dashboard-02' },
- { id: 'panel-01' },
- { id: 'index-*' }
- ]));
-
- savedObjectsClient.bulkGet.returns(Promise.resolve({
- saved_objects: [
- { id: 'dashboard-01' }, { id: 'dashboard-02' }
- ]
- }));
- });
-
- afterEach(() => {
- collectPanelsStub.restore();
- savedObjectsClient.bulkGet.resetHistory();
- });
-
- it('should request all dashboards', async () => {
- await collectDashboards(savedObjectsClient, ids);
-
- expect(savedObjectsClient.bulkGet.calledOnce).to.equal(true);
-
- const args = savedObjectsClient.bulkGet.getCall(0).args;
- expect(args[0]).to.eql([{
- id: 'dashboard-01',
- type: 'dashboard'
- }, {
- id: 'dashboard-02',
- type: 'dashboard'
- }]);
- });
-
- it('should call collectPanels with dashboard docs', async () => {
- await collectDashboards(savedObjectsClient, ids);
-
- expect(collectPanelsStub.calledTwice).to.equal(true);
- expect(collectPanelsStub.args[0][1]).to.eql({ id: 'dashboard-01' });
- expect(collectPanelsStub.args[1][1]).to.eql({ id: 'dashboard-02' });
- });
-
- it('should return an unique list of objects', async () => {
- const results = await collectDashboards(savedObjectsClient, ids);
- expect(results).to.eql([
- { id: 'dashboard-01' },
- { id: 'panel-01' },
- { id: 'index-*' },
- { id: 'dashboard-02' },
- ]);
- });
-});
diff --git a/src/legacy/core_plugins/kibana/server/lib/export/__tests__/collect_index_patterns.js b/src/legacy/core_plugins/kibana/server/lib/export/__tests__/collect_index_patterns.js
deleted file mode 100644
index edbb8ecbf10b3..0000000000000
--- a/src/legacy/core_plugins/kibana/server/lib/export/__tests__/collect_index_patterns.js
+++ /dev/null
@@ -1,106 +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 sinon from 'sinon';
-import { collectIndexPatterns } from '../collect_index_patterns';
-import { expect } from 'chai';
-
-describe('collectIndexPatterns(req, panels)', () => {
- const panels = [
- {
- attributes: {
- kibanaSavedObjectMeta: {
- searchSourceJSON: JSON.stringify({ index: 'index-*' })
- }
- }
- }, {
- attributes: {
- kibanaSavedObjectMeta: {
- searchSourceJSON: JSON.stringify({ index: 'logstash-*' })
- }
- }
- }, {
- attributes: {
- kibanaSavedObjectMeta: {
- searchSourceJSON: JSON.stringify({ index: 'logstash-*' })
- }
- }
- }, {
- attributes: {
- savedSearchId: 1,
- kibanaSavedObjectMeta: {
- searchSourceJSON: JSON.stringify({ index: 'bad-*' })
- }
- }
- }
- ];
-
- const savedObjectsClient = { bulkGet: sinon.stub() };
-
- beforeEach(() => {
- savedObjectsClient.bulkGet.returns(Promise.resolve({
- saved_objects: [
- { id: 'index-*' }, { id: 'logstash-*' }
- ]
- }));
- });
-
- afterEach(() => {
- savedObjectsClient.bulkGet.resetHistory();
- });
-
- it('should request all index patterns', async () => {
- await collectIndexPatterns(savedObjectsClient, panels);
-
- expect(savedObjectsClient.bulkGet.calledOnce).to.equal(true);
- expect(savedObjectsClient.bulkGet.getCall(0).args[0]).to.eql([{
- id: 'index-*',
- type: 'index-pattern'
- }, {
- id: 'logstash-*',
- type: 'index-pattern'
- }]);
- });
-
- it('should return the index pattern docs', async () => {
- const results = await collectIndexPatterns(savedObjectsClient, panels);
-
- expect(results).to.eql([
- { id: 'index-*' },
- { id: 'logstash-*' }
- ]);
- });
-
- it('should return an empty array if nothing is requested', async () => {
- const input = [
- {
- attributes: {
- savedSearchId: 1,
- kibanaSavedObjectMeta: {
- searchSourceJSON: JSON.stringify({ index: 'bad-*' })
- }
- }
- }
- ];
-
- const results = await collectIndexPatterns(savedObjectsClient, input);
- expect(results).to.eql([]);
- expect(savedObjectsClient.bulkGet.calledOnce).to.eql(false);
- });
-});
diff --git a/src/legacy/core_plugins/kibana/server/lib/export/__tests__/collect_panels.js b/src/legacy/core_plugins/kibana/server/lib/export/__tests__/collect_panels.js
deleted file mode 100644
index f15d6a088f997..0000000000000
--- a/src/legacy/core_plugins/kibana/server/lib/export/__tests__/collect_panels.js
+++ /dev/null
@@ -1,105 +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 sinon from 'sinon';
-import * as collectIndexPatternsDep from '../collect_index_patterns';
-import * as collectSearchSourcesDep from '../collect_search_sources';
-import { collectPanels } from '../collect_panels';
-import { expect } from 'chai';
-
-describe('collectPanels(req, dashboard)', () => {
- let collectSearchSourcesStub;
- let collectIndexPatternsStub;
- let dashboard;
-
- const savedObjectsClient = { bulkGet: sinon.stub() };
-
- beforeEach(() => {
- dashboard = {
- attributes: {
- panelsJSON: JSON.stringify([
- { id: 'panel-01', type: 'search' },
- { id: 'panel-02', type: 'visualization' }
- ])
- }
- };
-
- savedObjectsClient.bulkGet.returns(Promise.resolve({
- saved_objects: [
- { id: 'panel-01' }, { id: 'panel-02' }
- ]
- }));
-
- collectIndexPatternsStub = sinon.stub(collectIndexPatternsDep, 'collectIndexPatterns');
- collectIndexPatternsStub.returns([{ id: 'logstash-*' }]);
- collectSearchSourcesStub = sinon.stub(collectSearchSourcesDep, 'collectSearchSources');
- collectSearchSourcesStub.returns([ { id: 'search-01' }]);
- });
-
- afterEach(() => {
- collectSearchSourcesStub.restore();
- collectIndexPatternsStub.restore();
- savedObjectsClient.bulkGet.resetHistory();
- });
-
- it('should request each panel in the panelJSON', async () => {
- await collectPanels(savedObjectsClient, dashboard);
-
- expect(savedObjectsClient.bulkGet.calledOnce).to.equal(true);
- expect(savedObjectsClient.bulkGet.getCall(0).args[0]).to.eql([{
- id: 'panel-01',
- type: 'search'
- }, {
- id: 'panel-02',
- type: 'visualization'
- }]);
- });
-
- it('should call collectSearchSources()', async () => {
- await collectPanels(savedObjectsClient, dashboard);
- expect(collectSearchSourcesStub.calledOnce).to.equal(true);
- expect(collectSearchSourcesStub.args[0][1]).to.eql([
- { id: 'panel-01' },
- { id: 'panel-02' }
- ]);
- });
-
- it('should call collectIndexPatterns()', async () => {
- await collectPanels(savedObjectsClient, dashboard);
-
- expect(collectIndexPatternsStub.calledOnce).to.equal(true);
- expect(collectIndexPatternsStub.args[0][1]).to.eql([
- { id: 'panel-01' },
- { id: 'panel-02' }
- ]);
- });
-
- it('should return panels, index patterns, search sources, and dashboard', async () => {
- const results = await collectPanels(savedObjectsClient, dashboard);
-
- expect(results).to.eql([
- { id: 'panel-01' },
- { id: 'panel-02' },
- { id: 'logstash-*' },
- { id: 'search-01' },
- dashboard
- ]);
- });
-
-});
diff --git a/src/legacy/core_plugins/kibana/server/lib/export/__tests__/collect_search_sources.js b/src/legacy/core_plugins/kibana/server/lib/export/__tests__/collect_search_sources.js
deleted file mode 100644
index 4f9de8a971b7c..0000000000000
--- a/src/legacy/core_plugins/kibana/server/lib/export/__tests__/collect_search_sources.js
+++ /dev/null
@@ -1,85 +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 sinon from 'sinon';
-import * as deps from '../collect_index_patterns';
-import { collectSearchSources } from '../collect_search_sources';
-import { expect } from 'chai';
-describe('collectSearchSources(req, panels)', () => {
- const savedObjectsClient = { bulkGet: sinon.stub() };
-
- let panels;
- let collectIndexPatternsStub;
-
- beforeEach(() => {
- panels = [
- { attributes: { savedSearchId: 1 } },
- { attributes: { savedSearchId: 2 } }
- ];
-
- collectIndexPatternsStub = sinon.stub(deps, 'collectIndexPatterns');
- collectIndexPatternsStub.returns(Promise.resolve([{ id: 'logstash-*' }]));
-
- savedObjectsClient.bulkGet.returns(Promise.resolve({
- saved_objects: [
- { id: 1 }, { id: 2 }
- ]
- }));
- });
-
- afterEach(() => {
- collectIndexPatternsStub.restore();
- savedObjectsClient.bulkGet.resetHistory();
- });
-
- it('should request all search sources', async () => {
- await collectSearchSources(savedObjectsClient, panels);
-
- expect(savedObjectsClient.bulkGet.calledOnce).to.equal(true);
- expect(savedObjectsClient.bulkGet.getCall(0).args[0]).to.eql([
- { type: 'search', id: 1 }, { type: 'search', id: 2 }
- ]);
- });
-
- it('should return the search source and index patterns', async () => {
- const results = await collectSearchSources(savedObjectsClient, panels);
-
- expect(results).to.eql([
- { id: 1 },
- { id: 2 },
- { id: 'logstash-*' }
- ]);
- });
-
- it('should return an empty array if nothing is requested', async () => {
- const input = [
- {
- attributes: {
- kibanaSavedObjectMeta: {
- searchSourceJSON: JSON.stringify({ index: 'bad-*' })
- }
- }
- }
- ];
-
- const results = await collectSearchSources(savedObjectsClient, input);
- expect(results).to.eql([]);
- expect(savedObjectsClient.bulkGet.calledOnce).to.eql(false);
- });
-});
diff --git a/src/legacy/core_plugins/kibana/server/lib/export/__tests__/export_dashboards.js b/src/legacy/core_plugins/kibana/server/lib/export/__tests__/export_dashboards.js
deleted file mode 100644
index 1fb93ec4d95c5..0000000000000
--- a/src/legacy/core_plugins/kibana/server/lib/export/__tests__/export_dashboards.js
+++ /dev/null
@@ -1,74 +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 * as deps from '../collect_dashboards';
-import { exportDashboards } from '../export_dashboards';
-import sinon from 'sinon';
-import { expect } from 'chai';
-
-describe('exportDashboards(req)', () => {
-
- let req;
- let collectDashboardsStub;
-
- beforeEach(() => {
- req = {
- query: { dashboard: 'dashboard-01' },
- server: {
- config: () => ({ get: () => '6.0.0' }),
- plugins: {
- elasticsearch: {
- getCluster: () => ({ callWithRequest: sinon.stub() })
- }
- },
- },
- getSavedObjectsClient() {
- return null;
- }
- };
-
- collectDashboardsStub = sinon.stub(deps, 'collectDashboards');
- collectDashboardsStub.returns(Promise.resolve([
- { id: 'dashboard-01' },
- { id: 'logstash-*' },
- { id: 'panel-01' }
- ]));
- });
-
- afterEach(() => {
- collectDashboardsStub.restore();
- });
-
- it('should return a response object with version', () => {
- return exportDashboards(req).then((resp) => {
- expect(resp).to.have.property('version', '6.0.0');
- });
- });
-
- it('should return a response object with objects', () => {
- return exportDashboards(req).then((resp) => {
- expect(resp).to.have.property('objects');
- expect(resp.objects).to.eql([
- { id: 'dashboard-01' },
- { id: 'logstash-*' },
- { id: 'panel-01' }
- ]);
- });
- });
-});
diff --git a/src/legacy/core_plugins/kibana/server/lib/export/collect_dashboards.js b/src/legacy/core_plugins/kibana/server/lib/export/collect_dashboards.js
deleted file mode 100644
index 969c2add1364d..0000000000000
--- a/src/legacy/core_plugins/kibana/server/lib/export/collect_dashboards.js
+++ /dev/null
@@ -1,43 +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 { collectPanels } from './collect_panels';
-
-export async function collectDashboards(savedObjectsClient, ids) {
-
- if (ids.length === 0) return [];
-
- const objects = ids.map(id => {
- return {
- type: 'dashboard',
- id: id
- };
- });
-
- const { saved_objects: savedObjects } = await savedObjectsClient.bulkGet(objects);
- const results = await Promise.all(savedObjects.map(d => collectPanels(savedObjectsClient, d)));
-
- return results
- .reduce((acc, result) => acc.concat(result), [])
- .reduce((acc, obj) => {
- if (!acc.find(o => o.id === obj.id)) acc.push(obj);
- return acc;
- }, []);
-
-}
diff --git a/src/legacy/core_plugins/kibana/server/lib/export/collect_index_patterns.js b/src/legacy/core_plugins/kibana/server/lib/export/collect_index_patterns.js
deleted file mode 100644
index 441871208dc08..0000000000000
--- a/src/legacy/core_plugins/kibana/server/lib/export/collect_index_patterns.js
+++ /dev/null
@@ -1,43 +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.
- */
-
-export async function collectIndexPatterns(savedObjectsClient, panels) {
- const docs = panels.reduce((acc, panel) => {
- const { kibanaSavedObjectMeta, savedSearchId } = panel.attributes;
-
- if (kibanaSavedObjectMeta && kibanaSavedObjectMeta.searchSourceJSON && !savedSearchId) {
- let searchSourceData;
- try {
- searchSourceData = JSON.parse(kibanaSavedObjectMeta.searchSourceJSON);
- } catch (err) {
- return acc;
- }
-
- if (searchSourceData.index && !acc.find(s => s.id === searchSourceData.index)) {
- acc.push({ type: 'index-pattern', id: searchSourceData.index });
- }
- }
- return acc;
- }, []);
-
- if (docs.length === 0) return [];
-
- const { saved_objects: savedObjects } = await savedObjectsClient.bulkGet(docs);
- return savedObjects;
-}
diff --git a/src/legacy/core_plugins/kibana/server/lib/export/collect_panels.js b/src/legacy/core_plugins/kibana/server/lib/export/collect_panels.js
deleted file mode 100644
index eba6e335818e3..0000000000000
--- a/src/legacy/core_plugins/kibana/server/lib/export/collect_panels.js
+++ /dev/null
@@ -1,43 +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 { get } from 'lodash';
-
-import { collectIndexPatterns } from './collect_index_patterns';
-import { collectSearchSources } from './collect_search_sources';
-
-
-export async function collectPanels(savedObjectsClient, dashboard) {
- let panels;
- try {
- panels = JSON.parse(get(dashboard, 'attributes.panelsJSON', '[]'));
- } catch(err) {
- panels = [];
- }
-
- if (panels.length === 0) return [].concat([dashboard]);
-
- const { saved_objects: savedObjects } = await savedObjectsClient.bulkGet(panels);
- const [ indexPatterns, searchSources ] = await Promise.all([
- collectIndexPatterns(savedObjectsClient, savedObjects),
- collectSearchSources(savedObjectsClient, savedObjects)
- ]);
-
- return savedObjects.concat(indexPatterns).concat(searchSources).concat([dashboard]);
-}
diff --git a/src/legacy/core_plugins/kibana/server/lib/export/collect_references_deep.test.ts b/src/legacy/core_plugins/kibana/server/lib/export/collect_references_deep.test.ts
new file mode 100644
index 0000000000000..f5b9b299fd766
--- /dev/null
+++ b/src/legacy/core_plugins/kibana/server/lib/export/collect_references_deep.test.ts
@@ -0,0 +1,196 @@
+/*
+ * 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 '../../../../../../server/saved_objects/service/saved_objects_client';
+import { collectReferencesDeep } from './collect_references_deep';
+
+const data = [
+ {
+ id: '1',
+ type: 'dashboard',
+ attributes: {
+ panelsJSON: JSON.stringify([{ panelRefName: 'panel_0' }, { panelRefName: 'panel_1' }]),
+ },
+ references: [
+ {
+ name: 'panel_0',
+ type: 'visualization',
+ id: '2',
+ },
+ {
+ name: 'panel_1',
+ type: 'visualization',
+ id: '3',
+ },
+ ],
+ },
+ {
+ id: '2',
+ type: 'visualization',
+ attributes: {
+ kibanaSavedObjectMeta: {
+ searchSourceJSON: JSON.stringify({
+ indexRefName: 'kibanaSavedObjectMeta.searchSourceJSON.index',
+ }),
+ },
+ },
+ references: [
+ {
+ name: 'kibanaSavedObjectMeta.searchSourceJSON.index',
+ type: 'index-pattern',
+ id: '4',
+ },
+ ],
+ },
+ {
+ id: '3',
+ type: 'visualization',
+ attributes: {
+ savedSearchRefName: 'search_0',
+ },
+ references: [
+ {
+ name: 'search_0',
+ type: 'search',
+ id: '5',
+ },
+ ],
+ },
+ {
+ id: '4',
+ type: 'index-pattern',
+ attributes: {
+ title: 'pattern*',
+ },
+ },
+ {
+ id: '5',
+ type: 'search',
+ attributes: {
+ kibanaSavedObjectMeta: {
+ searchSourceJSON: JSON.stringify({
+ indexRefName: 'kibanaSavedObjectMeta.searchSourceJSON.index',
+ }),
+ },
+ },
+ references: [
+ {
+ name: 'kibanaSavedObjectMeta.searchSourceJSON.index',
+ type: 'index-pattern',
+ id: '4',
+ },
+ ],
+ },
+];
+
+test('collects dashboard and all dependencies', async () => {
+ const savedObjectClient = {
+ errors: {} as any,
+ create: jest.fn(),
+ bulkCreate: jest.fn(),
+ delete: jest.fn(),
+ find: jest.fn(),
+ get: jest.fn(),
+ update: jest.fn(),
+ bulkGet: jest.fn(getObjects => {
+ return {
+ saved_objects: getObjects.map((obj: SavedObject) =>
+ data.find(row => row.id === obj.id && row.type === obj.type)
+ ),
+ };
+ }),
+ };
+ const objects = await collectReferencesDeep(savedObjectClient, [{ type: 'dashboard', id: '1' }]);
+ expect(objects).toMatchInlineSnapshot(`
+Array [
+ Object {
+ "attributes": Object {
+ "panelsJSON": "[{\\"panelRefName\\":\\"panel_0\\"},{\\"panelRefName\\":\\"panel_1\\"}]",
+ },
+ "id": "1",
+ "references": Array [
+ Object {
+ "id": "2",
+ "name": "panel_0",
+ "type": "visualization",
+ },
+ Object {
+ "id": "3",
+ "name": "panel_1",
+ "type": "visualization",
+ },
+ ],
+ "type": "dashboard",
+ },
+ Object {
+ "attributes": Object {
+ "kibanaSavedObjectMeta": Object {
+ "searchSourceJSON": "{\\"indexRefName\\":\\"kibanaSavedObjectMeta.searchSourceJSON.index\\"}",
+ },
+ },
+ "id": "2",
+ "references": Array [
+ Object {
+ "id": "4",
+ "name": "kibanaSavedObjectMeta.searchSourceJSON.index",
+ "type": "index-pattern",
+ },
+ ],
+ "type": "visualization",
+ },
+ Object {
+ "attributes": Object {
+ "savedSearchRefName": "search_0",
+ },
+ "id": "3",
+ "references": Array [
+ Object {
+ "id": "5",
+ "name": "search_0",
+ "type": "search",
+ },
+ ],
+ "type": "visualization",
+ },
+ Object {
+ "attributes": Object {
+ "title": "pattern*",
+ },
+ "id": "4",
+ "type": "index-pattern",
+ },
+ Object {
+ "attributes": Object {
+ "kibanaSavedObjectMeta": Object {
+ "searchSourceJSON": "{\\"indexRefName\\":\\"kibanaSavedObjectMeta.searchSourceJSON.index\\"}",
+ },
+ },
+ "id": "5",
+ "references": Array [
+ Object {
+ "id": "4",
+ "name": "kibanaSavedObjectMeta.searchSourceJSON.index",
+ "type": "index-pattern",
+ },
+ ],
+ "type": "search",
+ },
+]
+`);
+});
diff --git a/src/legacy/core_plugins/kibana/server/lib/export/collect_references_deep.ts b/src/legacy/core_plugins/kibana/server/lib/export/collect_references_deep.ts
new file mode 100644
index 0000000000000..f62fc3c474c08
--- /dev/null
+++ b/src/legacy/core_plugins/kibana/server/lib/export/collect_references_deep.ts
@@ -0,0 +1,55 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {
+ SavedObject,
+ SavedObjectsClient,
+} from '../../../../../../server/saved_objects/service/saved_objects_client';
+
+const MAX_BULK_GET_SIZE = 10000;
+
+interface ObjectsToCollect {
+ id: string;
+ type: string;
+}
+
+export async function collectReferencesDeep(
+ savedObjectClient: SavedObjectsClient,
+ objects: ObjectsToCollect[]
+) {
+ let result: SavedObject[] = [];
+ const queue = [...objects];
+ while (queue.length !== 0) {
+ const itemsToGet = queue.splice(0, MAX_BULK_GET_SIZE);
+ const { saved_objects: savedObjects } = await savedObjectClient.bulkGet(itemsToGet);
+ result = result.concat(savedObjects);
+ for (const { references = [] } of savedObjects) {
+ for (const reference of references) {
+ const isDuplicate = queue
+ .concat(result)
+ .some(obj => obj.type === reference.type && obj.id === reference.id);
+ if (isDuplicate) {
+ continue;
+ }
+ queue.push({ type: reference.type, id: reference.id });
+ }
+ }
+ }
+ return result;
+}
diff --git a/src/legacy/core_plugins/kibana/server/lib/export/collect_search_sources.js b/src/legacy/core_plugins/kibana/server/lib/export/collect_search_sources.js
deleted file mode 100644
index 38471e9b6012c..0000000000000
--- a/src/legacy/core_plugins/kibana/server/lib/export/collect_search_sources.js
+++ /dev/null
@@ -1,39 +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 { collectIndexPatterns } from './collect_index_patterns';
-
-export async function collectSearchSources(savedObjectsClient, panels) {
- const docs = panels.reduce((acc, panel) => {
- const { savedSearchId } = panel.attributes;
- if (savedSearchId) {
- if (!acc.find(s => s.id === savedSearchId) && !panels.find(p => p.id === savedSearchId)) {
- acc.push({ type: 'search', id: savedSearchId });
- }
- }
- return acc;
- }, []);
-
- if (docs.length === 0) return [];
-
- const { saved_objects: savedObjects } = await savedObjectsClient.bulkGet(docs);
- const indexPatterns = await collectIndexPatterns(savedObjectsClient, savedObjects);
-
- return savedObjects.concat(indexPatterns);
-}
diff --git a/src/legacy/core_plugins/kibana/server/lib/export/export_dashboards.js b/src/legacy/core_plugins/kibana/server/lib/export/export_dashboards.js
index ed61d2c83366a..07b1bafd61c91 100644
--- a/src/legacy/core_plugins/kibana/server/lib/export/export_dashboards.js
+++ b/src/legacy/core_plugins/kibana/server/lib/export/export_dashboards.js
@@ -18,7 +18,7 @@
*/
import _ from 'lodash';
-import { collectDashboards } from './collect_dashboards';
+import { collectReferencesDeep } from './collect_references_deep';
export async function exportDashboards(req) {
@@ -26,8 +26,9 @@ export async function exportDashboards(req) {
const config = req.server.config();
const savedObjectsClient = req.getSavedObjectsClient();
+ const objectsToExport = ids.map(id => ({ id, type: 'dashboard' }));
- const objects = await collectDashboards(savedObjectsClient, ids);
+ const objects = await collectReferencesDeep(savedObjectsClient, objectsToExport);
return {
version: config.get('pkg.version'),
objects
diff --git a/src/legacy/core_plugins/kibana/server/lib/management/saved_objects/relationships.js b/src/legacy/core_plugins/kibana/server/lib/management/saved_objects/relationships.js
index e98bebe4c4066..afed200476343 100644
--- a/src/legacy/core_plugins/kibana/server/lib/management/saved_objects/relationships.js
+++ b/src/legacy/core_plugins/kibana/server/lib/management/saved_objects/relationships.js
@@ -17,166 +17,45 @@
* under the License.
*/
-async function findDashboardRelationships(id, size, savedObjectsClient) {
- const dashboard = await savedObjectsClient.get('dashboard', id);
- const visualizations = [];
-
- // TODO: should we handle exceptions here or at the parent level?
- const panelsJSON = JSON.parse(dashboard.attributes.panelsJSON);
- if (panelsJSON) {
- const visualizationIds = panelsJSON.map(panel => panel.id);
- const visualizationResponse = await savedObjectsClient.bulkGet(
- visualizationIds.slice(0, size).map(id => ({
- id,
- type: 'visualization',
- }))
- );
-
- visualizations.push(
- ...visualizationResponse.saved_objects.reduce((accum, object) => {
- if (!object.error) {
- accum.push({
- id: object.id,
- title: object.attributes.title,
- });
- }
- return accum;
- }, [])
- );
- }
-
- return { visualizations };
-}
-
-async function findVisualizationRelationships(id, size, savedObjectsClient) {
- await savedObjectsClient.get('visualization', id);
- const allDashboardsResponse = await savedObjectsClient.find({
- type: 'dashboard',
- fields: ['title', 'panelsJSON'],
- });
-
- const dashboards = [];
- for (const dashboard of allDashboardsResponse.saved_objects) {
- if (dashboard.error) {
- continue;
- }
- const panelsJSON = JSON.parse(dashboard.attributes.panelsJSON);
- if (panelsJSON) {
- for (const panel of panelsJSON) {
- if (panel.type === 'visualization' && panel.id === id) {
- dashboards.push({
- id: dashboard.id,
- title: dashboard.attributes.title,
- });
- }
- }
- }
-
- if (dashboards.length >= size) {
- break;
- }
- }
- return { dashboards };
-}
-
-async function findSavedSearchRelationships(id, size, savedObjectsClient) {
- const search = await savedObjectsClient.get('search', id);
-
- const searchSourceJSON = JSON.parse(search.attributes.kibanaSavedObjectMeta.searchSourceJSON);
-
- const indexPatterns = [];
- try {
- const indexPattern = await savedObjectsClient.get('index-pattern', searchSourceJSON.index);
- indexPatterns.push({ id: indexPattern.id, title: indexPattern.attributes.title });
- } catch (err) {
- // Do nothing
- }
-
- const allVisualizationsResponse = await savedObjectsClient.find({
- type: 'visualization',
- searchFields: ['savedSearchId'],
- search: id,
- fields: ['title'],
- });
-
- const visualizations = allVisualizationsResponse.saved_objects.reduce((accum, object) => {
- if (!object.error) {
- accum.push({
- id: object.id,
- title: object.attributes.title,
- });
- }
- return accum;
- }, []);
-
- return { visualizations, indexPatterns };
-}
-
-async function findIndexPatternRelationships(id, size, savedObjectsClient) {
- await savedObjectsClient.get('index-pattern', id);
- const [allVisualizationsResponse, savedSearchResponse] = await Promise.all([
+export async function findRelationships(type, id, options = {}) {
+ const {
+ size,
+ savedObjectsClient,
+ savedObjectTypes,
+ } = options;
+
+ const { references = [] } = await savedObjectsClient.get(type, id);
+ const bulkGetOpts = references.map(ref => ({ id: ref.id, type: ref.type }));
+
+ const [referencedObjects, referencedResponse] = await Promise.all([
+ bulkGetOpts.length > 0
+ ? savedObjectsClient.bulkGet(bulkGetOpts)
+ : Promise.resolve({ saved_objects: [] }),
savedObjectsClient.find({
- type: 'visualization',
- searchFields: ['kibanaSavedObjectMeta.searchSourceJSON'],
- search: '*',
- fields: [`title`, `kibanaSavedObjectMeta.searchSourceJSON`],
- }),
- savedObjectsClient.find({
- type: 'search',
- searchFields: ['kibanaSavedObjectMeta.searchSourceJSON'],
- search: '*',
- fields: [`title`, `kibanaSavedObjectMeta.searchSourceJSON`],
+ hasReference: { type, id },
+ perPage: size,
+ fields: ['title'],
+ type: savedObjectTypes,
}),
]);
- const visualizations = [];
- for (const visualization of allVisualizationsResponse.saved_objects) {
- if (visualization.error) {
- continue;
- }
- const searchSourceJSON = JSON.parse(visualization.attributes.kibanaSavedObjectMeta.searchSourceJSON);
- if (searchSourceJSON && searchSourceJSON.index === id) {
- visualizations.push({
- id: visualization.id,
- title: visualization.attributes.title,
- });
- }
-
- if (visualizations.length >= size) {
- break;
- }
- }
-
- const searches = [];
- for (const search of savedSearchResponse.saved_objects) {
- if (search.error) {
- continue;
- }
- const searchSourceJSON = JSON.parse(search.attributes.kibanaSavedObjectMeta.searchSourceJSON);
- if (searchSourceJSON && searchSourceJSON.index === id) {
- searches.push({
- id: search.id,
- title: search.attributes.title,
- });
- }
-
- if (searches.length >= size) {
- break;
- }
- }
- return { visualizations, searches };
+ const relationshipObjects = [].concat(
+ referencedObjects.saved_objects.map(extractCommonProperties),
+ referencedResponse.saved_objects.map(extractCommonProperties),
+ );
+
+ return relationshipObjects.reduce((result, relationshipObject) => {
+ const objectsForType = (result[relationshipObject.type] || []);
+ const { type, ...relationshipObjectWithoutType } = relationshipObject;
+ result[type] = objectsForType.concat(relationshipObjectWithoutType);
+ return result;
+ }, {});
}
-export async function findRelationships(type, id, size, savedObjectsClient) {
- switch (type) {
- case 'dashboard':
- return await findDashboardRelationships(id, size, savedObjectsClient);
- case 'visualization':
- return await findVisualizationRelationships(id, size, savedObjectsClient);
- case 'search':
- return await findSavedSearchRelationships(id, size, savedObjectsClient);
- case 'index-pattern':
- return await findIndexPatternRelationships(id, size, savedObjectsClient);
- }
- return {};
+function extractCommonProperties(savedObject) {
+ return {
+ id: savedObject.id,
+ type: savedObject.type,
+ title: savedObject.attributes.title,
+ };
}
diff --git a/src/legacy/core_plugins/kibana/server/routes/api/management/saved_objects/relationships.js b/src/legacy/core_plugins/kibana/server/routes/api/management/saved_objects/relationships.js
index d88cc05c97f8d..268f16b4f4afa 100644
--- a/src/legacy/core_plugins/kibana/server/routes/api/management/saved_objects/relationships.js
+++ b/src/legacy/core_plugins/kibana/server/routes/api/management/saved_objects/relationships.js
@@ -17,10 +17,8 @@
* under the License.
*/
-import Boom from 'boom';
import Joi from 'joi';
import { findRelationships } from '../../../../lib/management/saved_objects/relationships';
-import { isNotFoundError } from '../../../../../../../../server/saved_objects/service/lib/errors';
export function registerRelationships(server) {
server.route({
@@ -33,7 +31,7 @@ export function registerRelationships(server) {
id: Joi.string(),
}),
query: Joi.object().keys({
- size: Joi.number(),
+ size: Joi.number().default(10000),
}),
},
},
@@ -41,17 +39,15 @@ export function registerRelationships(server) {
handler: async (req) => {
const type = req.params.type;
const id = req.params.id;
- const size = req.query.size || 10;
+ const size = req.query.size;
+ const savedObjectsClient = req.getSavedObjectsClient();
- try {
- return await findRelationships(type, id, size, req.getSavedObjectsClient());
- } catch (err) {
- if (isNotFoundError(err)) {
- throw Boom.boomify(new Error('Resource not found'), { statusCode: 404 });
- }
-
- throw Boom.boomify(err, { statusCode: 500 });
- }
+ return await findRelationships(type, id, {
+ size,
+ savedObjectsClient,
+ // Pass in all types except space, spaces wrapper will throw error
+ savedObjectTypes: server.savedObjects.types.filter(type => type !== 'space'),
+ });
},
});
}
diff --git a/src/legacy/core_plugins/kibana/server/routes/api/management/saved_objects/scroll.js b/src/legacy/core_plugins/kibana/server/routes/api/management/saved_objects/scroll.js
index 76cc66c05385c..a2cc63b4b8679 100644
--- a/src/legacy/core_plugins/kibana/server/routes/api/management/saved_objects/scroll.js
+++ b/src/legacy/core_plugins/kibana/server/routes/api/management/saved_objects/scroll.js
@@ -62,6 +62,7 @@ export function registerScrollForExportRoute(server) {
savedObjectVersion: 2
},
_migrationVersion: hit.migrationVersion,
+ _references: hit.references || [],
};
});
}
diff --git a/src/server/saved_objects/migrations/core/__snapshots__/build_active_mappings.test.ts.snap b/src/server/saved_objects/migrations/core/__snapshots__/build_active_mappings.test.ts.snap
index 156c5fa6887f4..7e6b8bf5e9e58 100644
--- a/src/server/saved_objects/migrations/core/__snapshots__/build_active_mappings.test.ts.snap
+++ b/src/server/saved_objects/migrations/core/__snapshots__/build_active_mappings.test.ts.snap
@@ -25,6 +25,20 @@ Object {
"namespace": Object {
"type": "keyword",
},
+ "references": Object {
+ "properties": Object {
+ "id": Object {
+ "type": "keyword",
+ },
+ "name": Object {
+ "type": "keyword",
+ },
+ "type": Object {
+ "type": "keyword",
+ },
+ },
+ "type": "nested",
+ },
"type": Object {
"type": "keyword",
},
diff --git a/src/server/saved_objects/migrations/core/build_active_mappings.ts b/src/server/saved_objects/migrations/core/build_active_mappings.ts
index 156f47faecd56..eae75b418ed54 100644
--- a/src/server/saved_objects/migrations/core/build_active_mappings.ts
+++ b/src/server/saved_objects/migrations/core/build_active_mappings.ts
@@ -74,6 +74,20 @@ function defaultMapping(): IndexMapping {
updated_at: {
type: 'date',
},
+ references: {
+ type: 'nested',
+ properties: {
+ name: {
+ type: 'keyword',
+ },
+ type: {
+ type: 'keyword',
+ },
+ id: {
+ type: 'keyword',
+ },
+ },
+ },
},
};
}
diff --git a/src/server/saved_objects/migrations/core/document_migrator.test.ts b/src/server/saved_objects/migrations/core/document_migrator.test.ts
index cdd895594e4dd..293d96c8e62f4 100644
--- a/src/server/saved_objects/migrations/core/document_migrator.test.ts
+++ b/src/server/saved_objects/migrations/core/document_migrator.test.ts
@@ -19,7 +19,7 @@
import _ from 'lodash';
import sinon from 'sinon';
-import { SavedObjectDoc } from '../../serialization';
+import { RawSavedObjectDoc } from '../../serialization';
import { DocumentMigrator } from './document_migrator';
describe('DocumentMigrator', () => {
@@ -486,13 +486,13 @@ describe('DocumentMigrator', () => {
...testOpts(),
migrations: {
aaa: {
- '1.2.3': (doc: SavedObjectDoc) => doc,
- '10.4.0': (doc: SavedObjectDoc) => doc,
- '2.2.1': (doc: SavedObjectDoc) => doc,
+ '1.2.3': (doc: RawSavedObjectDoc) => doc,
+ '10.4.0': (doc: RawSavedObjectDoc) => doc,
+ '2.2.1': (doc: RawSavedObjectDoc) => doc,
},
bbb: {
- '3.2.3': (doc: SavedObjectDoc) => doc,
- '2.0.0': (doc: SavedObjectDoc) => doc,
+ '3.2.3': (doc: RawSavedObjectDoc) => doc,
+ '2.0.0': (doc: RawSavedObjectDoc) => doc,
},
},
});
@@ -525,11 +525,11 @@ describe('DocumentMigrator', () => {
});
function renameAttr(path: string, newPath: string) {
- return (doc: SavedObjectDoc) =>
- _.omit(_.set(doc, newPath, _.get(doc, path)), path) as SavedObjectDoc;
+ return (doc: RawSavedObjectDoc) =>
+ _.omit(_.set(doc, newPath, _.get(doc, path)), path) as RawSavedObjectDoc;
}
function setAttr(path: string, value: any) {
- return (doc: SavedObjectDoc) =>
- _.set(doc, path, _.isFunction(value) ? value(_.get(doc, path)) : value) as SavedObjectDoc;
+ return (doc: RawSavedObjectDoc) =>
+ _.set(doc, path, _.isFunction(value) ? value(_.get(doc, path)) : value) as RawSavedObjectDoc;
}
diff --git a/src/server/saved_objects/migrations/core/document_migrator.ts b/src/server/saved_objects/migrations/core/document_migrator.ts
index 0e0631aa8fce2..4a2a5aedc250d 100644
--- a/src/server/saved_objects/migrations/core/document_migrator.ts
+++ b/src/server/saved_objects/migrations/core/document_migrator.ts
@@ -63,12 +63,12 @@
import Boom from 'boom';
import _ from 'lodash';
import Semver from 'semver';
-import { MigrationVersion, SavedObjectDoc } from '../../serialization';
+import { MigrationVersion, RawSavedObjectDoc } from '../../serialization';
import { LogFn, Logger, MigrationLogger } from './migration_logger';
-export type TransformFn = (doc: SavedObjectDoc) => SavedObjectDoc;
+export type TransformFn = (doc: RawSavedObjectDoc) => RawSavedObjectDoc;
-type ValidateDoc = (doc: SavedObjectDoc) => void;
+type ValidateDoc = (doc: RawSavedObjectDoc) => void;
interface MigrationDefinition {
[type: string]: { [version: string]: TransformFn };
@@ -142,11 +142,11 @@ export class DocumentMigrator implements VersionedTransformer {
/**
* Migrates a document to the latest version.
*
- * @param {SavedObjectDoc} doc
- * @returns {SavedObjectDoc}
+ * @param {RawSavedObjectDoc} doc
+ * @returns {RawSavedObjectDoc}
* @memberof DocumentMigrator
*/
- public migrate = (doc: SavedObjectDoc): SavedObjectDoc => {
+ public migrate = (doc: RawSavedObjectDoc): RawSavedObjectDoc => {
return this.transformDoc(doc);
};
}
@@ -225,7 +225,7 @@ function buildDocumentTransform({
migrations: ActiveMigrations;
validateDoc: ValidateDoc;
}): TransformFn {
- return function transformAndValidate(doc: SavedObjectDoc) {
+ return function transformAndValidate(doc: RawSavedObjectDoc) {
const result = doc.migrationVersion
? applyMigrations(doc, migrations)
: markAsUpToDate(doc, migrations);
@@ -243,7 +243,7 @@ function buildDocumentTransform({
};
}
-function applyMigrations(doc: SavedObjectDoc, migrations: ActiveMigrations) {
+function applyMigrations(doc: RawSavedObjectDoc, migrations: ActiveMigrations) {
while (true) {
const prop = nextUnmigratedProp(doc, migrations);
if (!prop) {
@@ -256,14 +256,14 @@ function applyMigrations(doc: SavedObjectDoc, migrations: ActiveMigrations) {
/**
* Gets the doc's props, handling the special case of "type".
*/
-function props(doc: SavedObjectDoc) {
+function props(doc: RawSavedObjectDoc) {
return Object.keys(doc).concat(doc.type);
}
/**
* Looks up the prop version in a saved object document or in our latest migrations.
*/
-function propVersion(doc: SavedObjectDoc | ActiveMigrations, prop: string) {
+function propVersion(doc: RawSavedObjectDoc | ActiveMigrations, prop: string) {
return (
(doc[prop] && doc[prop].latestVersion) ||
(doc.migrationVersion && (doc as any).migrationVersion[prop])
@@ -273,7 +273,7 @@ function propVersion(doc: SavedObjectDoc | ActiveMigrations, prop: string) {
/**
* Sets the doc's migrationVersion to be the most recent version
*/
-function markAsUpToDate(doc: SavedObjectDoc, migrations: ActiveMigrations) {
+function markAsUpToDate(doc: RawSavedObjectDoc, migrations: ActiveMigrations) {
return {
...doc,
migrationVersion: props(doc).reduce((acc, prop) => {
@@ -288,7 +288,7 @@ function markAsUpToDate(doc: SavedObjectDoc, migrations: ActiveMigrations) {
* about the document and transform that caused the failure.
*/
function wrapWithTry(version: string, prop: string, transform: TransformFn, log: Logger) {
- return function tryTransformDoc(doc: SavedObjectDoc) {
+ return function tryTransformDoc(doc: RawSavedObjectDoc) {
try {
const result = transform(doc);
@@ -313,7 +313,7 @@ function wrapWithTry(version: string, prop: string, transform: TransformFn, log:
/**
* Finds the first unmigrated property in the specified document.
*/
-function nextUnmigratedProp(doc: SavedObjectDoc, migrations: ActiveMigrations) {
+function nextUnmigratedProp(doc: RawSavedObjectDoc, migrations: ActiveMigrations) {
return props(doc).find(p => {
const latestVersion = propVersion(migrations, p);
const docVersion = propVersion(doc, p);
@@ -343,10 +343,10 @@ function nextUnmigratedProp(doc: SavedObjectDoc, migrations: ActiveMigrations) {
* Applies any relevent migrations to the document for the specified property.
*/
function migrateProp(
- doc: SavedObjectDoc,
+ doc: RawSavedObjectDoc,
prop: string,
migrations: ActiveMigrations
-): SavedObjectDoc {
+): RawSavedObjectDoc {
const originalType = doc.type;
let migrationVersion = _.clone(doc.migrationVersion) || {};
const typeChanged = () => !doc.hasOwnProperty(prop) || doc.type !== originalType;
@@ -367,7 +367,7 @@ function migrateProp(
/**
* Retrieves any prop transforms that have not been applied to doc.
*/
-function applicableTransforms(migrations: ActiveMigrations, doc: SavedObjectDoc, prop: string) {
+function applicableTransforms(migrations: ActiveMigrations, doc: RawSavedObjectDoc, prop: string) {
const minVersion = propVersion(doc, prop);
const { transforms } = migrations[prop];
return minVersion
@@ -380,7 +380,7 @@ function applicableTransforms(migrations: ActiveMigrations, doc: SavedObjectDoc,
* has not mutated migrationVersion in an unsupported way.
*/
function updateMigrationVersion(
- doc: SavedObjectDoc,
+ doc: RawSavedObjectDoc,
migrationVersion: MigrationVersion,
prop: string,
version: string
@@ -396,7 +396,7 @@ function updateMigrationVersion(
* as this could get us into an infinite loop. So, we explicitly check for that here.
*/
function assertNoDowngrades(
- doc: SavedObjectDoc,
+ doc: RawSavedObjectDoc,
migrationVersion: MigrationVersion,
prop: string,
version: string
diff --git a/src/server/saved_objects/migrations/core/index_migrator.test.ts b/src/server/saved_objects/migrations/core/index_migrator.test.ts
index a5be8e5216773..68e13bfbb53e2 100644
--- a/src/server/saved_objects/migrations/core/index_migrator.test.ts
+++ b/src/server/saved_objects/migrations/core/index_migrator.test.ts
@@ -20,7 +20,7 @@
import _ from 'lodash';
import sinon from 'sinon';
import { SavedObjectsSchema } from '../../schema';
-import { SavedObjectDoc, SavedObjectsSerializer } from '../../serialization';
+import { RawSavedObjectDoc, SavedObjectsSerializer } from '../../serialization';
import { CallCluster } from './call_cluster';
import { IndexMigrator } from './index_migrator';
@@ -50,6 +50,14 @@ describe('IndexMigrator', () => {
namespace: { type: 'keyword' },
type: { type: 'keyword' },
updated_at: { type: 'date' },
+ references: {
+ type: 'nested',
+ properties: {
+ name: { type: 'keyword' },
+ type: { type: 'keyword' },
+ id: { type: 'keyword' },
+ },
+ },
},
},
include_type_name: true,
@@ -83,6 +91,14 @@ describe('IndexMigrator', () => {
namespace: { type: 'keyword' },
type: { type: 'keyword' },
updated_at: { type: 'date' },
+ references: {
+ type: 'nested',
+ properties: {
+ name: { type: 'keyword' },
+ type: { type: 'keyword' },
+ id: { type: 'keyword' },
+ },
+ },
},
},
settings: { number_of_shards: 1, auto_expand_replicas: '0-1' },
@@ -191,6 +207,14 @@ describe('IndexMigrator', () => {
namespace: { type: 'keyword' },
type: { type: 'keyword' },
updated_at: { type: 'date' },
+ references: {
+ type: 'nested',
+ properties: {
+ name: { type: 'keyword' },
+ type: { type: 'keyword' },
+ id: { type: 'keyword' },
+ },
+ },
},
},
settings: { number_of_shards: 1, auto_expand_replicas: '0-1' },
@@ -240,7 +264,7 @@ describe('IndexMigrator', () => {
let count = 0;
const opts = defaultOpts();
const callCluster = clusterStub(opts);
- const migrateDoc = sinon.spy((doc: SavedObjectDoc) => ({
+ const migrateDoc = sinon.spy((doc: RawSavedObjectDoc) => ({
...doc,
attributes: { name: ++count },
}));
@@ -266,24 +290,26 @@ describe('IndexMigrator', () => {
type: 'foo',
attributes: { name: 'Bar' },
migrationVersion: {},
+ references: [],
});
sinon.assert.calledWith(migrateDoc, {
id: '2',
type: 'foo',
attributes: { name: 'Baz' },
migrationVersion: {},
+ references: [],
});
expect(callCluster.args.filter(([action]) => action === 'bulk').length).toEqual(2);
sinon.assert.calledWith(callCluster, 'bulk', {
body: [
{ index: { _id: 'foo:1', _index: '.kibana_2' } },
- { foo: { name: 1 }, type: 'foo', migrationVersion: {} },
+ { foo: { name: 1 }, type: 'foo', migrationVersion: {}, references: [] },
],
});
sinon.assert.calledWith(callCluster, 'bulk', {
body: [
{ index: { _id: 'foo:2', _index: '.kibana_2' } },
- { foo: { name: 2 }, type: 'foo', migrationVersion: {} },
+ { foo: { name: 2 }, type: 'foo', migrationVersion: {}, references: [] },
],
});
});
diff --git a/src/server/saved_objects/migrations/core/migrate_raw_docs.test.ts b/src/server/saved_objects/migrations/core/migrate_raw_docs.test.ts
index 01bd409da8b69..26ccb4085e2a9 100644
--- a/src/server/saved_objects/migrations/core/migrate_raw_docs.test.ts
+++ b/src/server/saved_objects/migrations/core/migrate_raw_docs.test.ts
@@ -34,11 +34,11 @@ describe('migrateRawDocs', () => {
expect(result).toEqual([
{
_id: 'a:b',
- _source: { type: 'a', a: { name: 'HOI!' }, migrationVersion: {} },
+ _source: { type: 'a', a: { name: 'HOI!' }, migrationVersion: {}, references: [] },
},
{
_id: 'c:d',
- _source: { type: 'c', c: { name: 'HOI!' }, migrationVersion: {} },
+ _source: { type: 'c', c: { name: 'HOI!' }, migrationVersion: {}, references: [] },
},
]);
@@ -56,7 +56,7 @@ describe('migrateRawDocs', () => {
{ _id: 'foo:b', _source: { type: 'a', a: { name: 'AAA' } } },
{
_id: 'c:d',
- _source: { type: 'c', c: { name: 'TADA' }, migrationVersion: {} },
+ _source: { type: 'c', c: { name: 'TADA' }, migrationVersion: {}, references: [] },
},
]);
@@ -69,6 +69,7 @@ describe('migrateRawDocs', () => {
name: 'DDD',
},
migrationVersion: {},
+ references: [],
},
],
]);
diff --git a/src/server/saved_objects/migrations/core/migrate_raw_docs.ts b/src/server/saved_objects/migrations/core/migrate_raw_docs.ts
index ffafa9908d23e..c008b18619629 100644
--- a/src/server/saved_objects/migrations/core/migrate_raw_docs.ts
+++ b/src/server/saved_objects/migrations/core/migrate_raw_docs.ts
@@ -41,7 +41,10 @@ export function migrateRawDocs(
if (serializer.isRawSavedObject(raw)) {
const savedObject = serializer.rawToSavedObject(raw);
savedObject.migrationVersion = savedObject.migrationVersion || {};
- return serializer.savedObjectToRaw(migrateDoc(savedObject));
+ return serializer.savedObjectToRaw({
+ references: [],
+ ...migrateDoc(savedObject),
+ });
}
return raw;
diff --git a/src/server/saved_objects/migrations/kibana/__snapshots__/kibana_migrator.test.ts.snap b/src/server/saved_objects/migrations/kibana/__snapshots__/kibana_migrator.test.ts.snap
index 1733f94ded552..9627a351f55d4 100644
--- a/src/server/saved_objects/migrations/kibana/__snapshots__/kibana_migrator.test.ts.snap
+++ b/src/server/saved_objects/migrations/kibana/__snapshots__/kibana_migrator.test.ts.snap
@@ -25,6 +25,20 @@ Object {
"namespace": Object {
"type": "keyword",
},
+ "references": Object {
+ "properties": Object {
+ "id": Object {
+ "type": "keyword",
+ },
+ "name": Object {
+ "type": "keyword",
+ },
+ "type": Object {
+ "type": "keyword",
+ },
+ },
+ "type": "nested",
+ },
"type": Object {
"type": "keyword",
},
diff --git a/src/server/saved_objects/migrations/kibana/kibana_migrator.ts b/src/server/saved_objects/migrations/kibana/kibana_migrator.ts
index 06ff383133d35..3eddd46586d6b 100644
--- a/src/server/saved_objects/migrations/kibana/kibana_migrator.ts
+++ b/src/server/saved_objects/migrations/kibana/kibana_migrator.ts
@@ -24,7 +24,7 @@
import { once } from 'lodash';
import { SavedObjectsSchema, SavedObjectsSchemaDefinition } from '../../schema';
-import { SavedObjectDoc, SavedObjectsSerializer } from '../../serialization';
+import { RawSavedObjectDoc, SavedObjectsSerializer } from '../../serialization';
import { docValidator } from '../../validation';
import { buildActiveMappings, CallCluster, IndexMigrator, LogFn, MappingProperties } from '../core';
import { DocumentMigrator, VersionedTransformer } from '../core/document_migrator';
@@ -146,11 +146,11 @@ export class KibanaMigrator {
/**
* Migrates an individual doc to the latest version, as defined by the plugin migrations.
*
- * @param {SavedObjectDoc} doc
- * @returns {SavedObjectDoc}
+ * @param {RawSavedObjectDoc} doc
+ * @returns {RawSavedObjectDoc}
* @memberof KibanaMigrator
*/
- public migrateDocument(doc: SavedObjectDoc): SavedObjectDoc {
+ public migrateDocument(doc: RawSavedObjectDoc): RawSavedObjectDoc {
return this.documentMigrator.migrate(doc);
}
}
diff --git a/src/server/saved_objects/routes/bulk_create.js b/src/server/saved_objects/routes/bulk_create.js
index 943b2520a331d..d2dc14e3f6f7b 100644
--- a/src/server/saved_objects/routes/bulk_create.js
+++ b/src/server/saved_objects/routes/bulk_create.js
@@ -37,6 +37,14 @@ export const createBulkCreateRoute = prereqs => ({
attributes: Joi.object().required(),
version: Joi.number(),
migrationVersion: Joi.object().optional(),
+ references: Joi.array().items(
+ Joi.object()
+ .keys({
+ name: Joi.string().required(),
+ type: Joi.string().required(),
+ id: Joi.string().required(),
+ }),
+ ).default([]),
}).required()
),
},
diff --git a/src/server/saved_objects/routes/bulk_get.test.js b/src/server/saved_objects/routes/bulk_get.test.js
index e646629970fd4..62c155a57a165 100644
--- a/src/server/saved_objects/routes/bulk_get.test.js
+++ b/src/server/saved_objects/routes/bulk_get.test.js
@@ -59,7 +59,8 @@ describe('POST /api/saved_objects/_bulk_get', () => {
id: 'abc123',
type: 'index-pattern',
title: 'logstash-*',
- version: 2
+ version: 2,
+ references: [],
}]
};
diff --git a/src/server/saved_objects/routes/create.js b/src/server/saved_objects/routes/create.js
index 46d47d0f33891..a93e23a57550b 100644
--- a/src/server/saved_objects/routes/create.js
+++ b/src/server/saved_objects/routes/create.js
@@ -40,14 +40,22 @@ export const createCreateRoute = prereqs => {
payload: Joi.object({
attributes: Joi.object().required(),
migrationVersion: Joi.object().optional(),
+ references: Joi.array().items(
+ Joi.object()
+ .keys({
+ name: Joi.string().required(),
+ type: Joi.string().required(),
+ id: Joi.string().required(),
+ }),
+ ).default([]),
}).required(),
},
handler(request) {
const { savedObjectsClient } = request.pre;
const { type, id } = request.params;
const { overwrite } = request.query;
- const { migrationVersion } = request.payload;
- const options = { id, overwrite, migrationVersion };
+ const { migrationVersion, references } = request.payload;
+ const options = { id, overwrite, migrationVersion, references };
return savedObjectsClient.create(type, request.payload.attributes, options);
},
diff --git a/src/server/saved_objects/routes/create.test.js b/src/server/saved_objects/routes/create.test.js
index 48b6cb970e56b..4e35e2e3b38d4 100644
--- a/src/server/saved_objects/routes/create.test.js
+++ b/src/server/saved_objects/routes/create.test.js
@@ -57,7 +57,8 @@ describe('POST /api/saved_objects/{type}', () => {
const clientResponse = {
type: 'index-pattern',
id: 'logstash-*',
- title: 'Testing'
+ title: 'Testing',
+ references: [],
};
savedObjectsClient.create.returns(Promise.resolve(clientResponse));
@@ -100,7 +101,7 @@ describe('POST /api/saved_objects/{type}', () => {
expect(savedObjectsClient.create.calledOnce).toBe(true);
const args = savedObjectsClient.create.getCall(0).args;
- const options = { overwrite: false, id: undefined, migrationVersion: undefined };
+ const options = { overwrite: false, id: undefined, migrationVersion: undefined, references: [] };
const attributes = { title: 'Testing' };
expect(args).toEqual(['index-pattern', attributes, options]);
@@ -121,7 +122,7 @@ describe('POST /api/saved_objects/{type}', () => {
expect(savedObjectsClient.create.calledOnce).toBe(true);
const args = savedObjectsClient.create.getCall(0).args;
- const options = { overwrite: false, id: 'logstash-*' };
+ const options = { overwrite: false, id: 'logstash-*', references: [] };
const attributes = { title: 'Testing' };
expect(args).toEqual(['index-pattern', attributes, options]);
diff --git a/src/server/saved_objects/routes/find.js b/src/server/saved_objects/routes/find.js
index 27a30f0aa6427..2dd92c01e9867 100644
--- a/src/server/saved_objects/routes/find.js
+++ b/src/server/saved_objects/routes/find.js
@@ -34,6 +34,11 @@ export const createFindRoute = (prereqs) => ({
default_search_operator: Joi.string().valid('OR', 'AND').default('OR'),
search_fields: Joi.array().items(Joi.string()).single(),
sort_field: Joi.array().items(Joi.string()).single(),
+ has_reference: Joi.object()
+ .keys({
+ type: Joi.string().required(),
+ id: Joi.string().required(),
+ }).optional(),
fields: Joi.array().items(Joi.string()).single()
}).default()
},
diff --git a/src/server/saved_objects/routes/find.test.js b/src/server/saved_objects/routes/find.test.js
index 5c865c22f426b..d11ac8f00cb54 100644
--- a/src/server/saved_objects/routes/find.test.js
+++ b/src/server/saved_objects/routes/find.test.js
@@ -74,13 +74,15 @@ describe('GET /api/saved_objects/_find', () => {
id: 'logstash-*',
title: 'logstash-*',
timeFieldName: '@timestamp',
- notExpandable: true
+ notExpandable: true,
+ references: [],
}, {
type: 'index-pattern',
id: 'stocks-*',
title: 'stocks-*',
timeFieldName: '@timestamp',
- notExpandable: true
+ notExpandable: true,
+ references: [],
}
]
};
diff --git a/src/server/saved_objects/routes/get.test.js b/src/server/saved_objects/routes/get.test.js
index 63246bcf3a329..52ccb38601fb7 100644
--- a/src/server/saved_objects/routes/get.test.js
+++ b/src/server/saved_objects/routes/get.test.js
@@ -53,7 +53,8 @@ describe('GET /api/saved_objects/{type}/{id}', () => {
id: 'logstash-*',
title: 'logstash-*',
timeFieldName: '@timestamp',
- notExpandable: true
+ notExpandable: true,
+ references: [],
};
savedObjectsClient.get.returns(Promise.resolve(clientResponse));
diff --git a/src/server/saved_objects/routes/update.js b/src/server/saved_objects/routes/update.js
index cc7e2c0a4b4d1..163fb77d5b62f 100644
--- a/src/server/saved_objects/routes/update.js
+++ b/src/server/saved_objects/routes/update.js
@@ -32,14 +32,22 @@ export const createUpdateRoute = (prereqs) => {
}).required(),
payload: Joi.object({
attributes: Joi.object().required(),
- version: Joi.number().min(1)
+ version: Joi.number().min(1),
+ references: Joi.array().items(
+ Joi.object()
+ .keys({
+ name: Joi.string().required(),
+ type: Joi.string().required(),
+ id: Joi.string().required(),
+ }),
+ ).default([]),
}).required()
},
handler(request) {
const { savedObjectsClient } = request.pre;
const { type, id } = request.params;
- const { attributes, version } = request.payload;
- const options = { version };
+ const { attributes, version, references } = request.payload;
+ const options = { version, references };
return savedObjectsClient.update(type, id, attributes, options);
}
diff --git a/src/server/saved_objects/routes/update.test.js b/src/server/saved_objects/routes/update.test.js
index ac4d938794fda..72a540979c2de 100644
--- a/src/server/saved_objects/routes/update.test.js
+++ b/src/server/saved_objects/routes/update.test.js
@@ -51,7 +51,8 @@ describe('PUT /api/saved_objects/{type}/{id?}', () => {
payload: {
attributes: {
title: 'Testing'
- }
+ },
+ references: [],
}
};
@@ -66,7 +67,7 @@ describe('PUT /api/saved_objects/{type}/{id?}', () => {
it('calls upon savedObjectClient.update', async () => {
const attributes = { title: 'Testing' };
- const options = { version: 2 };
+ const options = { version: 2, references: [] };
const request = {
method: 'PUT',
url: '/api/saved_objects/index-pattern/logstash-*',
diff --git a/src/server/saved_objects/serialization/index.ts b/src/server/saved_objects/serialization/index.ts
index 09b8ddd94325a..94807ddeb7668 100644
--- a/src/server/saved_objects/serialization/index.ts
+++ b/src/server/saved_objects/serialization/index.ts
@@ -43,13 +43,22 @@ export interface MigrationVersion {
[type: string]: string;
}
+/**
+ * A reference object to anohter saved object.
+ */
+export interface SavedObjectReference {
+ name: string;
+ type: string;
+ id: string;
+}
+
/**
* A saved object type definition that allows for miscellaneous, unknown
* properties, as current discussions around security, ACLs, etc indicate
* that future props are likely to be added. Migrations support this
* scenario out of the box.
*/
-export interface SavedObjectDoc {
+interface SavedObjectDoc {
attributes: object;
id: string;
type: string;
@@ -61,6 +70,19 @@ export interface SavedObjectDoc {
[rootProp: string]: any;
}
+interface Referencable {
+ references: SavedObjectReference[];
+}
+
+/**
+ * We want to have two types, one that guarantees a "references" attribute
+ * will exist and one that allows it to be null. Since we're not migrating
+ * all the saved objects to have a "references" array, we need to support
+ * the scenarios where it may be missing (ex migrations).
+ */
+export type RawSavedObjectDoc = SavedObjectDoc & Partial;
+export type SanitizedSavedObjectDoc = SavedObjectDoc & Referencable;
+
function assertNonEmptyString(value: string, name: string) {
if (!value || typeof value !== 'string') {
throw new TypeError(`Expected "${value}" to be a ${name}`);
@@ -94,13 +116,14 @@ export class SavedObjectsSerializer {
*
* @param {RawDoc} rawDoc - The raw ES document to be converted to saved object format.
*/
- public rawToSavedObject({ _id, _source, _version }: RawDoc): SavedObjectDoc {
+ public rawToSavedObject({ _id, _source, _version }: RawDoc): SanitizedSavedObjectDoc {
const { type, namespace } = _source;
return {
type,
id: this.trimIdPrefix(namespace, type, _id),
...(namespace && !this.schema.isNamespaceAgnostic(type) && { namespace }),
attributes: _source[type],
+ references: _source.references || [],
...(_source.migrationVersion && { migrationVersion: _source.migrationVersion }),
...(_source.updated_at && { updated_at: _source.updated_at }),
...(_version != null && { version: _version }),
@@ -110,13 +133,23 @@ export class SavedObjectsSerializer {
/**
* Converts a document from the saved object client format to the format that is stored in elasticsearch.
*
- * @param {SavedObjectDoc} savedObj - The saved object to be converted to raw ES format.
+ * @param {SanitizedSavedObjectDoc} savedObj - The saved object to be converted to raw ES format.
*/
- public savedObjectToRaw(savedObj: SavedObjectDoc): RawDoc {
- const { id, type, namespace, attributes, migrationVersion, updated_at, version } = savedObj;
+ public savedObjectToRaw(savedObj: SanitizedSavedObjectDoc): RawDoc {
+ const {
+ id,
+ type,
+ namespace,
+ attributes,
+ migrationVersion,
+ updated_at,
+ version,
+ references,
+ } = savedObj;
const source = {
[type]: attributes,
type,
+ references,
...(namespace && !this.schema.isNamespaceAgnostic(type) && { namespace }),
...(migrationVersion && { migrationVersion }),
...(updated_at && { updated_at }),
diff --git a/src/server/saved_objects/serialization/serialization.test.ts b/src/server/saved_objects/serialization/serialization.test.ts
index 43ff4a2df7252..2178105fc289e 100644
--- a/src/server/saved_objects/serialization/serialization.test.ts
+++ b/src/server/saved_objects/serialization/serialization.test.ts
@@ -34,6 +34,24 @@ describe('saved object conversion', () => {
expect(actual).toHaveProperty('type', 'foo');
});
+ test('it copies the _source.references property to references', () => {
+ const serializer = new SavedObjectsSerializer(new SavedObjectsSchema());
+ const actual = serializer.rawToSavedObject({
+ _id: 'foo:bar',
+ _source: {
+ type: 'foo',
+ references: [{ name: 'ref_0', type: 'index-pattern', id: 'pattern*' }],
+ },
+ });
+ expect(actual).toHaveProperty('references', [
+ {
+ name: 'ref_0',
+ type: 'index-pattern',
+ id: 'pattern*',
+ },
+ ]);
+ });
+
test('if specified it copies the _source.migrationVersion property to migrationVersion', () => {
const serializer = new SavedObjectsSerializer(new SavedObjectsSchema());
const actual = serializer.rawToSavedObject({
@@ -95,6 +113,7 @@ describe('saved object conversion', () => {
acl: '33.3.5',
},
updated_at: now,
+ references: [],
};
expect(expected).toEqual(actual);
});
@@ -166,6 +185,7 @@ describe('saved object conversion', () => {
attributes: {
world: 'earth',
},
+ references: [],
});
});
@@ -180,6 +200,7 @@ describe('saved object conversion', () => {
expect(actual).toEqual({
id: 'universe',
type: 'hello',
+ references: [],
});
});
@@ -214,6 +235,7 @@ describe('saved object conversion', () => {
},
namespace: 'foo-namespace',
updated_at: new Date(),
+ references: [],
},
};
@@ -385,6 +407,23 @@ describe('saved object conversion', () => {
expect(actual._source).toHaveProperty('type', 'foo');
});
+ test('it copies the references property to _source.references', () => {
+ const serializer = new SavedObjectsSerializer(new SavedObjectsSchema());
+ const actual = serializer.savedObjectToRaw({
+ id: '1',
+ type: 'foo',
+ attributes: {},
+ references: [{ name: 'ref_0', type: 'index-pattern', id: 'pattern*' }],
+ });
+ expect(actual._source).toHaveProperty('references', [
+ {
+ name: 'ref_0',
+ type: 'index-pattern',
+ id: 'pattern*',
+ },
+ ]);
+ });
+
test('if specified it copies the updated_at property to _source.updated_at', () => {
const serializer = new SavedObjectsSerializer(new SavedObjectsSchema());
const now = new Date();
diff --git a/src/server/saved_objects/service/lib/repository.js b/src/server/saved_objects/service/lib/repository.js
index c047b6d72edc1..01b79b4050a55 100644
--- a/src/server/saved_objects/service/lib/repository.js
+++ b/src/server/saved_objects/service/lib/repository.js
@@ -72,6 +72,7 @@ export class SavedObjectsRepository {
* @property {boolean} [options.overwrite=false]
* @property {object} [options.migrationVersion=undefined]
* @property {string} [options.namespace]
+ * @property {array} [options.references] - [{ name, type, id }]
* @returns {promise} - { id, type, version, attributes }
*/
async create(type, attributes = {}, options = {}) {
@@ -80,6 +81,7 @@ export class SavedObjectsRepository {
migrationVersion,
overwrite = false,
namespace,
+ references = [],
} = options;
const method = id && !overwrite ? 'create' : 'index';
@@ -93,6 +95,7 @@ export class SavedObjectsRepository {
attributes,
migrationVersion,
updated_at: time,
+ references,
});
const raw = this._serializer.savedObjectToRaw(migrated);
@@ -122,11 +125,11 @@ export class SavedObjectsRepository {
/**
* Creates multiple documents at once
*
- * @param {array} objects - [{ type, id, attributes, migrationVersion }]
+ * @param {array} objects - [{ type, id, attributes, references, migrationVersion }]
* @param {object} [options={}]
* @property {boolean} [options.overwrite=false] - overwrites existing documents
* @property {string} [options.namespace]
- * @returns {promise} - {saved_objects: [[{ id, type, version, attributes, error: { message } }]}
+ * @returns {promise} - {saved_objects: [[{ id, type, version, references, attributes, error: { message } }]}
*/
async bulkCreate(objects, options = {}) {
const {
@@ -143,6 +146,7 @@ export class SavedObjectsRepository {
migrationVersion: object.migrationVersion,
namespace,
updated_at: time,
+ references: object.references || [],
});
const raw = this._serializer.savedObjectToRaw(migrated);
@@ -178,6 +182,7 @@ export class SavedObjectsRepository {
id = responseId,
type,
attributes,
+ references = [],
} = objects[i];
if (error) {
@@ -202,7 +207,8 @@ export class SavedObjectsRepository {
type,
updated_at: time,
version,
- attributes
+ attributes,
+ references,
};
})
};
@@ -292,6 +298,7 @@ export class SavedObjectsRepository {
* @property {string} [options.sortOrder]
* @property {Array} [options.fields]
* @property {string} [options.namespace]
+ * @property {object} [options.hasReference] - { type, id }
* @returns {promise} - { saved_objects: [{ id, type, version, attributes }], total, per_page, page }
*/
async find(options = {}) {
@@ -300,6 +307,7 @@ export class SavedObjectsRepository {
search,
defaultSearchOperator = 'OR',
searchFields,
+ hasReference,
page = 1,
perPage = 20,
sortField,
@@ -337,6 +345,7 @@ export class SavedObjectsRepository {
sortField,
sortOrder,
namespace,
+ hasReference,
})
}
};
@@ -414,6 +423,7 @@ export class SavedObjectsRepository {
...time && { updated_at: time },
version: doc._version,
attributes: doc._source[type],
+ references: doc._source.references || [],
migrationVersion: doc._source.migrationVersion,
};
})
@@ -456,6 +466,7 @@ export class SavedObjectsRepository {
...updatedAt && { updated_at: updatedAt },
version: response._version,
attributes: response._source[type],
+ references: response._source.references || [],
migrationVersion: response._source.migrationVersion,
};
}
@@ -468,12 +479,14 @@ export class SavedObjectsRepository {
* @param {object} [options={}]
* @property {integer} options.version - ensures version matches that of persisted object
* @property {string} [options.namespace]
+ * @property {array} [options.references] - [{ name, type, id }]
* @returns {promise}
*/
async update(type, id, attributes, options = {}) {
const {
version,
- namespace
+ namespace,
+ references = [],
} = options;
const time = this._getCurrentTime();
@@ -488,6 +501,7 @@ export class SavedObjectsRepository {
doc: {
[type]: attributes,
updated_at: time,
+ references,
}
},
});
@@ -502,6 +516,7 @@ export class SavedObjectsRepository {
type,
updated_at: time,
version: response._version,
+ references,
attributes
};
}
@@ -575,6 +590,7 @@ export class SavedObjectsRepository {
id,
type,
updated_at: time,
+ references: response.get._source.references,
version: response._version,
attributes: response.get._source[type],
};
diff --git a/src/server/saved_objects/service/lib/repository.test.js b/src/server/saved_objects/service/lib/repository.test.js
index 2862785694211..c5e1d609d2d58 100644
--- a/src/server/saved_objects/service/lib/repository.test.js
+++ b/src/server/saved_objects/service/lib/repository.test.js
@@ -259,6 +259,11 @@ describe('SavedObjectsRepository', () => {
}, {
id: 'logstash-*',
namespace: 'foo-namespace',
+ references: [{
+ name: 'ref_0',
+ type: 'test',
+ id: '123',
+ }],
});
expect(response).toEqual({
@@ -268,7 +273,12 @@ describe('SavedObjectsRepository', () => {
version: 2,
attributes: {
title: 'Logstash',
- }
+ },
+ references: [{
+ name: 'ref_0',
+ type: 'test',
+ id: '123',
+ }],
});
});
@@ -428,8 +438,8 @@ describe('SavedObjectsRepository', () => {
callAdminCluster.returns({ items: [] });
await savedObjectsRepository.bulkCreate([
- { type: 'config', id: 'one', attributes: { title: 'Test One' } },
- { type: 'index-pattern', id: 'two', attributes: { title: 'Test Two' } }
+ { type: 'config', id: 'one', attributes: { title: 'Test One' }, references: [{ name: 'ref_0', type: 'test', id: '1' }] },
+ { type: 'index-pattern', id: 'two', attributes: { title: 'Test Two' }, references: [{ name: 'ref_0', type: 'test', id: '2' }] },
]);
sinon.assert.calledOnce(callAdminCluster);
@@ -439,9 +449,14 @@ describe('SavedObjectsRepository', () => {
expect(bulkCalls[0][1].body).toEqual([
{ create: { _type: '_doc', _id: 'config:one' } },
- { type: 'config', ...mockTimestampFields, config: { title: 'Test One' } },
+ { type: 'config', ...mockTimestampFields, config: { title: 'Test One' }, references: [{ name: 'ref_0', type: 'test', id: '1' }] },
{ create: { _type: '_doc', _id: 'index-pattern:two' } },
- { type: 'index-pattern', ...mockTimestampFields, 'index-pattern': { title: 'Test Two' } }
+ {
+ type: 'index-pattern',
+ ...mockTimestampFields,
+ 'index-pattern': { title: 'Test Two' },
+ references: [{ name: 'ref_0', type: 'test', id: '2' }],
+ },
]);
sinon.assert.calledOnce(onBeforeWrite);
@@ -463,9 +478,21 @@ describe('SavedObjectsRepository', () => {
sinon.assert.calledWithExactly(callAdminCluster, 'bulk', sinon.match({
body: [
{ create: { _type: '_doc', _id: 'config:one' } },
- { type: 'config', ...mockTimestampFields, config: { title: 'Test One!!' }, migrationVersion: { foo: '2.3.4' } },
+ {
+ type: 'config',
+ ...mockTimestampFields,
+ config: { title: 'Test One!!' },
+ migrationVersion: { foo: '2.3.4' },
+ references: [],
+ },
{ create: { _type: '_doc', _id: 'index-pattern:two' } },
- { type: 'index-pattern', ...mockTimestampFields, 'index-pattern': { title: 'Test Two!!' }, migrationVersion: { foo: '2.3.4' } }
+ {
+ type: 'index-pattern',
+ ...mockTimestampFields,
+ 'index-pattern': { title: 'Test Two!!' },
+ migrationVersion: { foo: '2.3.4' },
+ references: [],
+ },
]
}));
});
@@ -479,7 +506,7 @@ describe('SavedObjectsRepository', () => {
body: [
// uses create because overwriting is not allowed
{ create: { _type: '_doc', _id: 'foo:bar' } },
- { type: 'foo', ...mockTimestampFields, 'foo': {} },
+ { type: 'foo', ...mockTimestampFields, 'foo': {}, references: [] },
]
}));
@@ -494,7 +521,7 @@ describe('SavedObjectsRepository', () => {
body: [
// uses index because overwriting is allowed
{ index: { _type: '_doc', _id: 'foo:bar' } },
- { type: 'foo', ...mockTimestampFields, 'foo': {} },
+ { type: 'foo', ...mockTimestampFields, 'foo': {}, references: [] },
]
}));
@@ -538,6 +565,7 @@ describe('SavedObjectsRepository', () => {
version: 2,
...mockTimestampFields,
attributes: { title: 'Test Two' },
+ references: [],
}
]
});
@@ -576,12 +604,14 @@ describe('SavedObjectsRepository', () => {
version: 2,
...mockTimestampFields,
attributes: { title: 'Test One' },
+ references: [],
}, {
id: 'two',
type: 'index-pattern',
version: 2,
...mockTimestampFields,
attributes: { title: 'Test Two' },
+ references: [],
}
]
});
@@ -602,9 +632,21 @@ describe('SavedObjectsRepository', () => {
sinon.assert.calledWithExactly(callAdminCluster, 'bulk', sinon.match({
body: [
{ create: { _type: '_doc', _id: 'foo-namespace:config:one' } },
- { namespace: 'foo-namespace', type: 'config', ...mockTimestampFields, config: { title: 'Test One' } },
+ {
+ namespace: 'foo-namespace',
+ type: 'config',
+ ...mockTimestampFields,
+ config: { title: 'Test One' },
+ references: [],
+ },
{ create: { _type: '_doc', _id: 'foo-namespace:index-pattern:two' } },
- { namespace: 'foo-namespace', type: 'index-pattern', ...mockTimestampFields, 'index-pattern': { title: 'Test Two' } }
+ {
+ namespace: 'foo-namespace',
+ type: 'index-pattern',
+ ...mockTimestampFields,
+ 'index-pattern': { title: 'Test Two' },
+ references: [],
+ },
]
}));
sinon.assert.calledOnce(onBeforeWrite);
@@ -620,9 +662,9 @@ describe('SavedObjectsRepository', () => {
sinon.assert.calledWithExactly(callAdminCluster, 'bulk', sinon.match({
body: [
{ create: { _type: '_doc', _id: 'config:one' } },
- { type: 'config', ...mockTimestampFields, config: { title: 'Test One' } },
+ { type: 'config', ...mockTimestampFields, config: { title: 'Test One' }, references: [] },
{ create: { _type: '_doc', _id: 'index-pattern:two' } },
- { type: 'index-pattern', ...mockTimestampFields, 'index-pattern': { title: 'Test Two' } }
+ { type: 'index-pattern', ...mockTimestampFields, 'index-pattern': { title: 'Test Two' }, references: [] }
]
}));
sinon.assert.calledOnce(onBeforeWrite);
@@ -642,7 +684,7 @@ describe('SavedObjectsRepository', () => {
sinon.assert.calledWithExactly(callAdminCluster, 'bulk', sinon.match({
body: [
{ create: { _type: '_doc', _id: 'globaltype:one' } },
- { type: 'globaltype', ...mockTimestampFields, 'globaltype': { title: 'Test One' } },
+ { type: 'globaltype', ...mockTimestampFields, 'globaltype': { title: 'Test One' }, references: [] },
]
}));
sinon.assert.calledOnce(onBeforeWrite);
@@ -812,22 +854,29 @@ describe('SavedObjectsRepository', () => {
}
});
- it('passes mappings, schema, search, defaultSearchOperator, searchFields, type, sortField, and sortOrder to getSearchDsl', async () => {
- callAdminCluster.returns(namespacedSearchResults);
- const relevantOpts = {
- namespace: 'foo-namespace',
- search: 'foo*',
- searchFields: ['foo'],
- type: 'bar',
- sortField: 'name',
- sortOrder: 'desc',
- defaultSearchOperator: 'AND',
- };
+ it(
+ 'passes mappings, schema, search, defaultSearchOperator, searchFields, type, sortField, sortOrder and hasReference to getSearchDsl',
+ async () => {
+ callAdminCluster.returns(namespacedSearchResults);
+ const relevantOpts = {
+ namespace: 'foo-namespace',
+ search: 'foo*',
+ searchFields: ['foo'],
+ type: 'bar',
+ sortField: 'name',
+ sortOrder: 'desc',
+ defaultSearchOperator: 'AND',
+ hasReference: {
+ type: 'foo',
+ id: '1',
+ },
+ };
- await savedObjectsRepository.find(relevantOpts);
- sinon.assert.calledOnce(getSearchDsl);
- sinon.assert.calledWithExactly(getSearchDsl, mappings, schema, relevantOpts);
- });
+ await savedObjectsRepository.find(relevantOpts);
+ sinon.assert.calledOnce(getSearchDsl);
+ sinon.assert.calledWithExactly(getSearchDsl, mappings, schema, relevantOpts);
+ }
+ );
it('merges output of getSearchDsl into es request body', async () => {
callAdminCluster.returns(noNamespaceSearchResults);
@@ -858,7 +907,8 @@ describe('SavedObjectsRepository', () => {
type: doc._source.type,
...mockTimestampFields,
version: doc._version,
- attributes: doc._source[doc._source.type]
+ attributes: doc._source[doc._source.type],
+ references: [],
});
});
});
@@ -881,7 +931,8 @@ describe('SavedObjectsRepository', () => {
type: doc._source.type,
...mockTimestampFields,
version: doc._version,
- attributes: doc._source[doc._source.type]
+ attributes: doc._source[doc._source.type],
+ references: [],
});
});
});
@@ -970,7 +1021,8 @@ describe('SavedObjectsRepository', () => {
version: 2,
attributes: {
title: 'Testing'
- }
+ },
+ references: [],
});
});
@@ -985,7 +1037,8 @@ describe('SavedObjectsRepository', () => {
version: 2,
attributes: {
title: 'Testing'
- }
+ },
+ references: [],
});
});
@@ -1132,7 +1185,8 @@ describe('SavedObjectsRepository', () => {
type: 'config',
...mockTimestampFields,
version: 2,
- attributes: { title: 'Test' }
+ attributes: { title: 'Test' },
+ references: [],
});
expect(savedObjects[1]).toEqual({
id: 'bad',
@@ -1168,13 +1222,25 @@ describe('SavedObjectsRepository', () => {
});
it('returns current ES document version', async () => {
- const response = await savedObjectsRepository.update('index-pattern', 'logstash-*', attributes, { namespace: 'foo-namespace' });
+ const response = await savedObjectsRepository.update('index-pattern', 'logstash-*', attributes, {
+ namespace: 'foo-namespace',
+ references: [{
+ name: 'ref_0',
+ type: 'test',
+ id: '1',
+ }],
+ });
expect(response).toEqual({
id,
type,
...mockTimestampFields,
version: newVersion,
- attributes
+ attributes,
+ references: [{
+ name: 'ref_0',
+ type: 'test',
+ id: '1',
+ }],
});
});
@@ -1197,6 +1263,11 @@ describe('SavedObjectsRepository', () => {
title: 'Testing',
}, {
namespace: 'foo-namespace',
+ references: [{
+ name: 'ref_0',
+ type: 'test',
+ id: '1',
+ }],
});
sinon.assert.calledOnce(callAdminCluster);
@@ -1205,7 +1276,15 @@ describe('SavedObjectsRepository', () => {
id: 'foo-namespace:index-pattern:logstash-*',
version: undefined,
body: {
- doc: { updated_at: mockTimestamp, 'index-pattern': { title: 'Testing' } }
+ doc: {
+ updated_at: mockTimestamp,
+ 'index-pattern': { title: 'Testing' },
+ references: [{
+ name: 'ref_0',
+ type: 'test',
+ id: '1',
+ }],
+ },
},
ignore: [404],
refresh: 'wait_for',
@@ -1216,7 +1295,15 @@ describe('SavedObjectsRepository', () => {
});
it(`doesn't prepend namespace to the id or add namespace property when providing no namespace for namespaced type`, async () => {
- await savedObjectsRepository.update('index-pattern', 'logstash-*', { title: 'Testing' });
+ await savedObjectsRepository.update('index-pattern', 'logstash-*', {
+ title: 'Testing',
+ }, {
+ references: [{
+ name: 'ref_0',
+ type: 'test',
+ id: '1',
+ }],
+ });
sinon.assert.calledOnce(callAdminCluster);
sinon.assert.calledWithExactly(callAdminCluster, 'update', {
@@ -1224,7 +1311,15 @@ describe('SavedObjectsRepository', () => {
id: 'index-pattern:logstash-*',
version: undefined,
body: {
- doc: { updated_at: mockTimestamp, 'index-pattern': { title: 'Testing' } }
+ doc: {
+ updated_at: mockTimestamp,
+ 'index-pattern': { title: 'Testing' },
+ references: [{
+ name: 'ref_0',
+ type: 'test',
+ id: '1',
+ }],
+ },
},
ignore: [404],
refresh: 'wait_for',
@@ -1239,6 +1334,11 @@ describe('SavedObjectsRepository', () => {
name: 'bar',
}, {
namespace: 'foo-namespace',
+ references: [{
+ name: 'ref_0',
+ type: 'test',
+ id: '1',
+ }],
});
sinon.assert.calledOnce(callAdminCluster);
@@ -1247,7 +1347,15 @@ describe('SavedObjectsRepository', () => {
id: 'globaltype:foo',
version: undefined,
body: {
- doc: { updated_at: mockTimestamp, 'globaltype': { name: 'bar' } }
+ doc: {
+ updated_at: mockTimestamp,
+ 'globaltype': { name: 'bar' },
+ references: [{
+ name: 'ref_0',
+ type: 'test',
+ id: '1',
+ }],
+ },
},
ignore: [404],
refresh: 'wait_for',
diff --git a/src/server/saved_objects/service/lib/search_dsl/query_params.js b/src/server/saved_objects/service/lib/search_dsl/query_params.js
index 47e5812e5eb2e..cd316593dde9e 100644
--- a/src/server/saved_objects/service/lib/search_dsl/query_params.js
+++ b/src/server/saved_objects/service/lib/search_dsl/query_params.js
@@ -99,13 +99,37 @@ function getClauseForType(schema, namespace, type) {
* @param {String} search
* @param {Array} searchFields
* @param {String} defaultSearchOperator
+ * @param {Object} hasReference
* @return {Object}
*/
-export function getQueryParams(mappings, schema, namespace, type, search, searchFields, defaultSearchOperator) {
+export function getQueryParams(mappings, schema, namespace, type, search, searchFields, defaultSearchOperator, hasReference) {
const types = getTypes(mappings, type);
const bool = {
filter: [{
bool: {
+ must: hasReference
+ ? [{
+ nested: {
+ path: 'references',
+ query: {
+ bool: {
+ must: [
+ {
+ term: {
+ 'references.id': hasReference.id,
+ },
+ },
+ {
+ term: {
+ 'references.type': hasReference.type,
+ },
+ },
+ ],
+ },
+ },
+ },
+ }]
+ : undefined,
should: types.map(type => getClauseForType(schema, namespace, type)),
minimum_should_match: 1
}
diff --git a/src/server/saved_objects/service/lib/search_dsl/query_params.test.js b/src/server/saved_objects/service/lib/search_dsl/query_params.test.js
index 8e98fe3a023cb..1d0a8063be992 100644
--- a/src/server/saved_objects/service/lib/search_dsl/query_params.test.js
+++ b/src/server/saved_objects/service/lib/search_dsl/query_params.test.js
@@ -778,4 +778,46 @@ describe('searchDsl/queryParams', () => {
});
});
});
+
+ describe('type (plural, namespaced and global), hasReference', () => {
+ it('supports hasReference', () => {
+ expect(getQueryParams(MAPPINGS, SCHEMA, 'foo-namespace', ['saved', 'global'], null, null, 'OR', { type: 'bar', id: '1' }))
+ .toEqual({
+ query: {
+ bool: {
+ filter: [{
+ bool: {
+ must: [{
+ nested: {
+ path: 'references',
+ query: {
+ bool: {
+ must: [
+ {
+ term: {
+ 'references.id': '1',
+ },
+ },
+ {
+ term: {
+ 'references.type': 'bar',
+ },
+ },
+ ],
+ },
+ },
+ },
+ }],
+ should: [
+ createTypeClause('saved', 'foo-namespace'),
+ createTypeClause('global'),
+ ],
+ minimum_should_match: 1,
+ }
+ }]
+ }
+ }
+ });
+ });
+ });
});
diff --git a/src/server/saved_objects/service/lib/search_dsl/search_dsl.js b/src/server/saved_objects/service/lib/search_dsl/search_dsl.js
index d6a224f4c3857..3a11cd0c8e91d 100644
--- a/src/server/saved_objects/service/lib/search_dsl/search_dsl.js
+++ b/src/server/saved_objects/service/lib/search_dsl/search_dsl.js
@@ -31,6 +31,7 @@ export function getSearchDsl(mappings, schema, options = {}) {
sortField,
sortOrder,
namespace,
+ hasReference,
} = options;
if (!type) {
@@ -42,7 +43,7 @@ export function getSearchDsl(mappings, schema, options = {}) {
}
return {
- ...getQueryParams(mappings, schema, namespace, type, search, searchFields, defaultSearchOperator),
+ ...getQueryParams(mappings, schema, namespace, type, search, searchFields, defaultSearchOperator, hasReference),
...getSortingParams(mappings, type, sortField, sortOrder),
};
}
diff --git a/src/server/saved_objects/service/lib/search_dsl/search_dsl.test.js b/src/server/saved_objects/service/lib/search_dsl/search_dsl.test.js
index 1ba780fc79ed0..0600c01848346 100644
--- a/src/server/saved_objects/service/lib/search_dsl/search_dsl.test.js
+++ b/src/server/saved_objects/service/lib/search_dsl/search_dsl.test.js
@@ -46,7 +46,7 @@ describe('getSearchDsl', () => {
});
describe('passes control', () => {
- it('passes (mappings, schema, namespace, type, search, searchFields) to getQueryParams', () => {
+ it('passes (mappings, schema, namespace, type, search, searchFields, hasReference) to getQueryParams', () => {
const spy = sandbox.spy(queryParamsNS, 'getQueryParams');
const mappings = { type: { properties: {} } };
const schema = { isNamespaceAgnostic: () => {} };
@@ -56,6 +56,10 @@ describe('getSearchDsl', () => {
search: 'bar',
searchFields: ['baz'],
defaultSearchOperator: 'AND',
+ hasReference: {
+ type: 'bar',
+ id: '1',
+ },
};
getSearchDsl(mappings, schema, opts);
@@ -69,6 +73,7 @@ describe('getSearchDsl', () => {
opts.search,
opts.searchFields,
opts.defaultSearchOperator,
+ opts.hasReference,
);
});
diff --git a/src/server/saved_objects/service/saved_objects_client.d.ts b/src/server/saved_objects/service/saved_objects_client.d.ts
index a6e10aa1b85b1..a58cec023b335 100644
--- a/src/server/saved_objects/service/saved_objects_client.d.ts
+++ b/src/server/saved_objects/service/saved_objects_client.d.ts
@@ -36,7 +36,7 @@ export interface BulkCreateObject {
}
export interface BulkCreateResponse {
- savedObjects: Array>;
+ saved_objects: Array>;
}
export interface FindOptions extends BaseOptions {
@@ -68,7 +68,7 @@ export interface BulkGetObject {
export type BulkGetObjects = BulkGetObject[];
export interface BulkGetResponse {
- savedObjects: Array>;
+ saved_objects: Array>;
}
export interface SavedObjectAttributes {
@@ -84,6 +84,13 @@ export interface SavedObject {
message: string;
};
attributes: T;
+ references: SavedObjectReference[];
+}
+
+export interface SavedObjectReference {
+ name: string;
+ type: string;
+ id: string;
}
export declare class SavedObjectsClient {
diff --git a/src/server/saved_objects/service/saved_objects_client.js b/src/server/saved_objects/service/saved_objects_client.js
index a354067e6f702..a18881ad09d41 100644
--- a/src/server/saved_objects/service/saved_objects_client.js
+++ b/src/server/saved_objects/service/saved_objects_client.js
@@ -148,6 +148,7 @@ export class SavedObjectsClient {
* @property {string} [options.sortOrder]
* @property {Array} [options.fields]
* @property {string} [options.namespace]
+ * @property {object} [options.hasReference] - { type, id }
* @returns {promise} - { saved_objects: [{ id, type, version, attributes }], total, per_page, page }
*/
async find(options = {}) {
diff --git a/src/ui/public/courier/saved_object/__tests__/saved_object.js b/src/ui/public/courier/saved_object/__tests__/saved_object.js
index 4d89ae5c19a66..9c0d092557b76 100644
--- a/src/ui/public/courier/saved_object/__tests__/saved_object.js
+++ b/src/ui/public/courier/saved_object/__tests__/saved_object.js
@@ -285,6 +285,32 @@ describe('Saved Object', function () {
});
});
});
+
+ describe('with extractReferences', () => {
+ it('calls the function', async () => {
+ const id = '123';
+ stubESResponse(getMockedDocResponse('id'));
+ let extractReferencesCallCount = 0;
+ const extractReferences = ({ attributes, references }) => {
+ extractReferencesCallCount++;
+ return { attributes, references };
+ };
+ return createInitializedSavedObject({ type: 'dashboard', extractReferences })
+ .then((savedObject) => {
+ sinon.stub(savedObjectsClientStub, 'create').callsFake(() => {
+ return BluebirdPromise.resolve({
+ id,
+ version: 2,
+ type: 'dashboard',
+ });
+ });
+ return savedObject.save();
+ })
+ .then(() => {
+ expect(extractReferencesCallCount).to.be(1);
+ });
+ });
+ });
});
describe('applyESResp', function () {
@@ -405,6 +431,73 @@ describe('Saved Object', function () {
});
});
+ it('does not inject references when references array is missing', async () => {
+ const injectReferences = sinon.stub();
+ const config = {
+ type: 'dashboard',
+ injectReferences,
+ };
+ const savedObject = new SavedObject(config);
+ return savedObject.init()
+ .then(() => {
+ const response = {
+ found: true,
+ _source: {
+ dinosaurs: { tRex: 'has big teeth' },
+ },
+ };
+ return savedObject.applyESResp(response);
+ })
+ .then(() => {
+ expect(injectReferences).to.have.property('notCalled', true);
+ });
+ });
+
+ it('does not inject references when references array is empty', async () => {
+ const injectReferences = sinon.stub();
+ const config = {
+ type: 'dashboard',
+ injectReferences,
+ };
+ const savedObject = new SavedObject(config);
+ return savedObject.init()
+ .then(() => {
+ const response = {
+ found: true,
+ _source: {
+ dinosaurs: { tRex: 'has big teeth' },
+ },
+ references: [],
+ };
+ return savedObject.applyESResp(response);
+ })
+ .then(() => {
+ expect(injectReferences).to.have.property('notCalled', true);
+ });
+ });
+
+ it('injects references when function is provided and references exist', async () => {
+ const injectReferences = sinon.stub();
+ const config = {
+ type: 'dashboard',
+ injectReferences,
+ };
+ const savedObject = new SavedObject(config);
+ return savedObject.init()
+ .then(() => {
+ const response = {
+ found: true,
+ _source: {
+ dinosaurs: { tRex: 'has big teeth' },
+ },
+ references: [{}],
+ };
+ return savedObject.applyESResp(response);
+ })
+ .then(() => {
+ expect(injectReferences).to.have.property('calledOnce', true);
+ });
+ });
});
describe ('config', function () {
diff --git a/src/ui/public/courier/saved_object/saved_object.js b/src/ui/public/courier/saved_object/saved_object.js
index edbfa6b898f5c..c3cf9be9491bb 100644
--- a/src/ui/public/courier/saved_object/saved_object.js
+++ b/src/ui/public/courier/saved_object/saved_object.js
@@ -103,6 +103,8 @@ export function SavedObjectProvider(Promise, Private, Notifier, confirmModalProm
const afterESResp = config.afterESResp || _.noop;
const customInit = config.init || _.noop;
+ const extractReferences = config.extractReferences;
+ const injectReferences = config.injectReferences;
// optional search source which this object configures
this.searchSource = config.searchSource ? new SearchSource() : undefined;
@@ -117,7 +119,7 @@ export function SavedObjectProvider(Promise, Private, Notifier, confirmModalProm
// in favor of a better rename/save flow.
this.copyOnSave = false;
- const parseSearchSource = (searchSourceJson) => {
+ const parseSearchSource = (searchSourceJson, references) => {
if (!this.searchSource) return;
// if we have a searchSource, set its values based on the searchSourceJson field
@@ -136,6 +138,16 @@ export function SavedObjectProvider(Promise, Private, Notifier, confirmModalProm
throw new InvalidJSONProperty(`Invalid searchSourceJSON in ${esType} "${this.id}".`);
}
+ // Inject index id if a reference is saved
+ if (searchSourceValues.indexRefName) {
+ const reference = references.find(reference => reference.name === searchSourceValues.indexRefName);
+ if (!reference) {
+ throw new Error(`Could not find reference for ${searchSourceValues.indexRefName} on ${this.getEsType()} ${this.id}`);
+ }
+ searchSourceValues.index = reference.id;
+ delete searchSourceValues.indexRefName;
+ }
+
const searchSourceFields = this.searchSource.getFields();
const fnProps = _.transform(searchSourceFields, function (dynamic, val, name) {
if (_.isFunction(val)) dynamic[name] = val;
@@ -213,6 +225,7 @@ export function SavedObjectProvider(Promise, Private, Notifier, confirmModalProm
_id: resp.id,
_type: resp.type,
_source: _.cloneDeep(resp.attributes),
+ references: resp.references,
found: resp._version ? true : false
};
})
@@ -254,8 +267,13 @@ export function SavedObjectProvider(Promise, Private, Notifier, confirmModalProm
this.lastSavedTitle = this.title;
return Promise.try(() => {
- parseSearchSource(meta.searchSourceJSON);
+ parseSearchSource(meta.searchSourceJSON, resp.references);
return this.hydrateIndexPattern();
+ }).then(() => {
+ if (injectReferences && resp.references && resp.references.length > 0) {
+ injectReferences(this, resp.references);
+ }
+ return this;
}).then(() => {
return Promise.cast(afterESResp.call(this, resp));
});
@@ -266,12 +284,13 @@ export function SavedObjectProvider(Promise, Private, Notifier, confirmModalProm
*
* @return {Object}
*/
- this.serialize = () => {
- const body = {};
+ this._serialize = () => {
+ const attributes = {};
+ const references = [];
_.forOwn(mapping, (fieldMapping, fieldName) => {
if (this[fieldName] != null) {
- body[fieldName] = (fieldMapping._serialize)
+ attributes[fieldName] = (fieldMapping._serialize)
? fieldMapping._serialize(this[fieldName])
: this[fieldName];
}
@@ -279,12 +298,22 @@ export function SavedObjectProvider(Promise, Private, Notifier, confirmModalProm
if (this.searchSource) {
const searchSourceFields = _.omit(this.searchSource.getFields(), ['sort', 'size']);
- body.kibanaSavedObjectMeta = {
+ if (searchSourceFields.index) {
+ const indexId = searchSourceFields.index;
+ searchSourceFields.indexRefName = 'kibanaSavedObjectMeta.searchSourceJSON.index';
+ references.push({
+ name: 'kibanaSavedObjectMeta.searchSourceJSON.index',
+ type: 'index-pattern',
+ id: indexId,
+ });
+ delete searchSourceFields.index;
+ }
+ attributes.kibanaSavedObjectMeta = {
searchSourceJSON: angular.toJson(searchSourceFields)
};
}
- return body;
+ return { attributes, references };
};
/**
@@ -304,16 +333,17 @@ export function SavedObjectProvider(Promise, Private, Notifier, confirmModalProm
/**
* Attempts to create the current object using the serialized source. If an object already
* exists, a warning message requests an overwrite confirmation.
- * @param source - serialized version of this object (return value from this.serialize())
+ * @param source - serialized version of this object (return value from this._serialize())
* What will be indexed into elasticsearch.
+ * @param options - options to pass to the saved object create method
* @returns {Promise} - A promise that is resolved with the objects id if the object is
* successfully indexed. If the overwrite confirmation was rejected, an error is thrown with
* a confirmRejected = true parameter so that case can be handled differently than
* a create or index error.
* @resolved {SavedObject}
*/
- const createSource = (source) => {
- return savedObjectsClient.create(esType, source, this.creationOpts())
+ const createSource = (source, options = {}) => {
+ return savedObjectsClient.create(esType, source, options)
.catch(err => {
// record exists, confirm overwriting
if (_.get(err, 'res.status') === 409) {
@@ -331,7 +361,7 @@ export function SavedObjectProvider(Promise, Private, Notifier, confirmModalProm
values: { name: this.getDisplayName() }
}),
})
- .then(() => savedObjectsClient.create(esType, source, this.creationOpts({ overwrite: true })))
+ .then(() => savedObjectsClient.create(esType, source, this.creationOpts({ overwrite: true, ...options })))
.catch(() => Promise.reject(new Error(OVERWRITE_REJECTED)));
}
return Promise.reject(err);
@@ -406,16 +436,21 @@ export function SavedObjectProvider(Promise, Private, Notifier, confirmModalProm
this.id = null;
}
- const source = this.serialize();
+ // Here we want to extract references and set them within "references" attribute
+ let { attributes, references } = this._serialize();
+ if (extractReferences) {
+ ({ attributes, references } = extractReferences({ attributes, references }));
+ }
+ if (!references) throw new Error('References not returned from extractReferences');
this.isSaving = true;
return checkForDuplicateTitle(isTitleDuplicateConfirmed, onTitleDuplicate)
.then(() => {
if (confirmOverwrite) {
- return createSource(source);
+ return createSource(attributes, this.creationOpts({ references }));
} else {
- return savedObjectsClient.create(esType, source, this.creationOpts({ overwrite: true }));
+ return savedObjectsClient.create(esType, attributes, this.creationOpts({ references, overwrite: true }));
}
})
.then((resp) => {
diff --git a/src/ui/public/saved_objects/saved_object.js b/src/ui/public/saved_objects/saved_object.js
index 9287b9e313d8f..ed3bbfe9b0fb1 100644
--- a/src/ui/public/saved_objects/saved_object.js
+++ b/src/ui/public/saved_objects/saved_object.js
@@ -20,11 +20,12 @@
import _ from 'lodash';
export class SavedObject {
- constructor(client, { id, type, version, attributes, error, migrationVersion } = {}) {
+ constructor(client, { id, type, version, attributes, error, migrationVersion, references } = {}) {
this._client = client;
this.id = id;
this.type = type;
this.attributes = attributes || {};
+ this.references = references || [];
this._version = version;
this.migrationVersion = migrationVersion;
if (error) {
@@ -46,9 +47,17 @@ export class SavedObject {
save() {
if (this.id) {
- return this._client.update(this.type, this.id, this.attributes, { migrationVersion: this.migrationVersion });
+ return this._client.update(
+ this.type,
+ this.id,
+ this.attributes,
+ {
+ migrationVersion: this.migrationVersion,
+ references: this.references,
+ },
+ );
} else {
- return this._client.create(this.type, this.attributes, { migrationVersion: this.migrationVersion });
+ return this._client.create(this.type, this.attributes, { migrationVersion: this.migrationVersion, references: this.references });
}
}
diff --git a/src/ui/public/saved_objects/saved_objects_client.js b/src/ui/public/saved_objects/saved_objects_client.js
index eba1acebabe4b..0ee4c2ff47ad3 100644
--- a/src/ui/public/saved_objects/saved_objects_client.js
+++ b/src/ui/public/saved_objects/saved_objects_client.js
@@ -51,6 +51,7 @@ export class SavedObjectsClient {
* @property {string} [options.id] - force id on creation, not recommended
* @property {boolean} [options.overwrite=false]
* @property {object} [options.migrationVersion]
+ * @property {array} [options.references] [{ name, type, id }]
* @returns {promise} - SavedObject({ id, type, version, attributes })
*/
create = (type, attributes = {}, options = {}) => {
@@ -61,7 +62,17 @@ export class SavedObjectsClient {
const path = this._getPath([type, options.id]);
const query = _.pick(options, ['overwrite']);
- return this._request({ method: 'POST', path, query, body: { attributes, migrationVersion: options.migrationVersion } })
+ return this
+ ._request({
+ method: 'POST',
+ path,
+ query,
+ body: {
+ attributes,
+ migrationVersion: options.migrationVersion,
+ references: options.references,
+ },
+ })
.catch(error => {
if (isAutoCreateIndexError(error)) {
return showAutoCreateIndexErrorPage();
@@ -75,7 +86,7 @@ export class SavedObjectsClient {
/**
* Creates multiple documents at once
*
- * @param {array} objects - [{ type, id, attributes, migrationVersion }]
+ * @param {array} objects - [{ type, id, attributes, references, migrationVersion }]
* @param {object} [options={}]
* @property {boolean} [options.overwrite=false]
* @returns {promise} - { savedObjects: [{ id, type, version, attributes, error: { message } }]}
@@ -117,6 +128,7 @@ export class SavedObjectsClient {
* @property {integer} [options.page=1]
* @property {integer} [options.perPage=20]
* @property {array} options.fields
+ * @property {object} [options.hasReference] - { type, id }
* @returns {promise} - { savedObjects: [ SavedObject({ id, type, version, attributes }) ]}
*/
find = (options = {}) => {
@@ -178,9 +190,10 @@ export class SavedObjectsClient {
* @param {object} options
* @prop {integer} options.version - ensures version matches that of persisted object
* @prop {object} options.migrationVersion - The optional migrationVersion of this document
+ * @prop {array} option.references - the references of the saved object
* @returns {promise}
*/
- update(type, id, attributes, { version, migrationVersion } = {}) {
+ update(type, id, attributes, { version, migrationVersion, references } = {}) {
if (!type || !id || !attributes) {
return Promise.reject(new Error('requires type, id and attributes'));
}
@@ -189,6 +202,7 @@ export class SavedObjectsClient {
const body = {
attributes,
migrationVersion,
+ references,
version
};
diff --git a/test/api_integration/apis/management/saved_objects/relationships.js b/test/api_integration/apis/management/saved_objects/relationships.js
index 45b0a8b82e4a2..e31a5fe3aaa7e 100644
--- a/test/api_integration/apis/management/saved_objects/relationships.js
+++ b/test/api_integration/apis/management/saved_objects/relationships.js
@@ -40,8 +40,8 @@ export default function ({ getService }) {
after(() => esArchiver.unload('management/saved_objects'));
const SEARCH_RESPONSE_SCHEMA = Joi.object().keys({
- visualizations: GENERIC_RESPONSE_SCHEMA,
- indexPatterns: GENERIC_RESPONSE_SCHEMA,
+ visualization: GENERIC_RESPONSE_SCHEMA,
+ 'index-pattern': GENERIC_RESPONSE_SCHEMA,
});
describe('searches', async () => {
@@ -61,13 +61,13 @@ export default function ({ getService }) {
.expect(200)
.then(resp => {
expect(resp.body).to.eql({
- visualizations: [
+ visualization: [
{
id: 'a42c0580-3224-11e8-a572-ffca06da1357',
title: 'VisualizationFromSavedSearch',
},
],
- indexPatterns: [
+ 'index-pattern': [
{
id: '8963ca30-3224-11e8-a572-ffca06da1357',
title: 'saved_objects*',
@@ -85,7 +85,7 @@ export default function ({ getService }) {
describe('dashboards', async () => {
const DASHBOARD_RESPONSE_SCHEMA = Joi.object().keys({
- visualizations: GENERIC_RESPONSE_SCHEMA,
+ visualization: GENERIC_RESPONSE_SCHEMA,
});
it('should validate dashboard response schema', async () => {
@@ -104,7 +104,7 @@ export default function ({ getService }) {
.expect(200)
.then(resp => {
expect(resp.body).to.eql({
- visualizations: [
+ visualization: [
{
id: 'add810b0-3224-11e8-a572-ffca06da1357',
title: 'Visualization',
@@ -128,7 +128,8 @@ export default function ({ getService }) {
describe('visualizations', async () => {
const VISUALIZATIONS_RESPONSE_SCHEMA = Joi.object().keys({
- dashboards: GENERIC_RESPONSE_SCHEMA,
+ dashboard: GENERIC_RESPONSE_SCHEMA,
+ search: GENERIC_RESPONSE_SCHEMA,
});
it('should validate visualization response schema', async () => {
@@ -147,7 +148,13 @@ export default function ({ getService }) {
.expect(200)
.then(resp => {
expect(resp.body).to.eql({
- dashboards: [
+ search: [
+ {
+ id: '960372e0-3224-11e8-a572-ffca06da1357',
+ title: 'OneRecord'
+ },
+ ],
+ dashboard: [
{
id: 'b70c7ae0-3224-11e8-a572-ffca06da1357',
title: 'Dashboard',
@@ -166,8 +173,8 @@ export default function ({ getService }) {
describe('index patterns', async () => {
const INDEX_PATTERN_RESPONSE_SCHEMA = Joi.object().keys({
- searches: GENERIC_RESPONSE_SCHEMA,
- visualizations: GENERIC_RESPONSE_SCHEMA,
+ search: GENERIC_RESPONSE_SCHEMA,
+ visualization: GENERIC_RESPONSE_SCHEMA,
});
it('should validate visualization response schema', async () => {
@@ -186,13 +193,13 @@ export default function ({ getService }) {
.expect(200)
.then(resp => {
expect(resp.body).to.eql({
- searches: [
+ search: [
{
id: '960372e0-3224-11e8-a572-ffca06da1357',
title: 'OneRecord',
},
],
- visualizations: [
+ visualization: [
{
id: 'add810b0-3224-11e8-a572-ffca06da1357',
title: 'Visualization',
diff --git a/test/api_integration/apis/saved_objects/bulk_create.js b/test/api_integration/apis/saved_objects/bulk_create.js
index 153dda4691fa6..074af9f775dde 100644
--- a/test/api_integration/apis/saved_objects/bulk_create.js
+++ b/test/api_integration/apis/saved_objects/bulk_create.js
@@ -69,7 +69,8 @@ export default function ({ getService }) {
version: 1,
attributes: {
title: 'A great new dashboard'
- }
+ },
+ references: [],
},
]
});
@@ -101,7 +102,8 @@ export default function ({ getService }) {
version: 1,
attributes: {
title: 'An existing visualization'
- }
+ },
+ references: [],
},
{
type: 'dashboard',
@@ -110,7 +112,8 @@ export default function ({ getService }) {
version: 1,
attributes: {
title: 'A great new dashboard'
- }
+ },
+ references: [],
},
]
});
diff --git a/test/api_integration/apis/saved_objects/bulk_get.js b/test/api_integration/apis/saved_objects/bulk_get.js
index 68773e5124039..da208a5cbe70a 100644
--- a/test/api_integration/apis/saved_objects/bulk_get.js
+++ b/test/api_integration/apis/saved_objects/bulk_get.js
@@ -68,7 +68,15 @@ export default function ({ getService }) {
visState: resp.body.saved_objects[0].attributes.visState,
uiStateJSON: resp.body.saved_objects[0].attributes.uiStateJSON,
kibanaSavedObjectMeta: resp.body.saved_objects[0].attributes.kibanaSavedObjectMeta
- }
+ },
+ migrationVersion: {
+ visualization: '7.0.0',
+ },
+ references: [{
+ name: 'kibanaSavedObjectMeta.searchSourceJSON.index',
+ type: 'index-pattern',
+ id: '91200a00-9efd-11e7-acb3-3dab96693fab',
+ }],
},
{
id: 'does not exist',
@@ -86,7 +94,8 @@ export default function ({ getService }) {
attributes: {
buildNum: 8467,
defaultIndex: '91200a00-9efd-11e7-acb3-3dab96693fab'
- }
+ },
+ references: [],
}
]
});
diff --git a/test/api_integration/apis/saved_objects/create.js b/test/api_integration/apis/saved_objects/create.js
index 379539928ebaf..a4e66318fb01e 100644
--- a/test/api_integration/apis/saved_objects/create.js
+++ b/test/api_integration/apis/saved_objects/create.js
@@ -54,7 +54,11 @@ export default function ({ getService }) {
version: 1,
attributes: {
title: 'My favorite vis'
- }
+ },
+ migrationVersion: {
+ visualization: '7.0.0',
+ },
+ references: [],
});
});
});
@@ -95,7 +99,11 @@ export default function ({ getService }) {
version: 1,
attributes: {
title: 'My favorite vis'
- }
+ },
+ migrationVersion: {
+ visualization: '7.0.0',
+ },
+ references: [],
});
});
diff --git a/test/api_integration/apis/saved_objects/find.js b/test/api_integration/apis/saved_objects/find.js
index c9b1e9fc73f4a..515f4501517bc 100644
--- a/test/api_integration/apis/saved_objects/find.js
+++ b/test/api_integration/apis/saved_objects/find.js
@@ -45,7 +45,8 @@ export default function ({ getService }) {
version: 1,
attributes: {
'title': 'Count of requests'
- }
+ },
+ references: [],
}
]
});
diff --git a/test/api_integration/apis/saved_objects/get.js b/test/api_integration/apis/saved_objects/get.js
index 22ea4798f4324..0734918f5f3e4 100644
--- a/test/api_integration/apis/saved_objects/get.js
+++ b/test/api_integration/apis/saved_objects/get.js
@@ -42,6 +42,9 @@ export default function ({ getService }) {
type: 'visualization',
updated_at: '2017-09-21T18:51:23.794Z',
version: resp.body.version,
+ migrationVersion: {
+ visualization: '7.0.0',
+ },
attributes: {
title: 'Count of requests',
description: '',
@@ -50,7 +53,12 @@ export default function ({ getService }) {
visState: resp.body.attributes.visState,
uiStateJSON: resp.body.attributes.uiStateJSON,
kibanaSavedObjectMeta: resp.body.attributes.kibanaSavedObjectMeta
- }
+ },
+ references: [{
+ type: 'index-pattern',
+ name: 'kibanaSavedObjectMeta.searchSourceJSON.index',
+ id: '91200a00-9efd-11e7-acb3-3dab96693fab',
+ }],
});
})
));
diff --git a/test/api_integration/apis/saved_objects/migrations.js b/test/api_integration/apis/saved_objects/migrations.js
index 260609d3b881e..a4993658837f2 100644
--- a/test/api_integration/apis/saved_objects/migrations.js
+++ b/test/api_integration/apis/saved_objects/migrations.js
@@ -85,11 +85,11 @@ export default ({ getService }) => {
// The docs in the alias have been migrated
assert.deepEqual(await fetchDocs({ callCluster, index }), [
- { id: 'bar:i', type: 'bar', migrationVersion: { bar: '1.9.0' }, bar: { mynum: 68 } },
- { id: 'bar:o', type: 'bar', migrationVersion: { bar: '1.9.0' }, bar: { mynum: 6 } },
- { id: 'baz:u', type: 'baz', baz: { title: 'Terrific!' } },
- { id: 'foo:a', type: 'foo', migrationVersion: { foo: '1.0.0' }, foo: { name: 'FOO A' } },
- { id: 'foo:e', type: 'foo', migrationVersion: { foo: '1.0.0' }, foo: { name: 'FOOEY' } },
+ { id: 'bar:i', type: 'bar', migrationVersion: { bar: '1.9.0' }, bar: { mynum: 68 }, references: [] },
+ { id: 'bar:o', type: 'bar', migrationVersion: { bar: '1.9.0' }, bar: { mynum: 6 }, references: [] },
+ { id: 'baz:u', type: 'baz', baz: { title: 'Terrific!' }, references: [] },
+ { id: 'foo:a', type: 'foo', migrationVersion: { foo: '1.0.0' }, foo: { name: 'FOO A' }, references: [] },
+ { id: 'foo:e', type: 'foo', migrationVersion: { foo: '1.0.0' }, foo: { name: 'FOOEY' }, references: [] },
]);
});
@@ -131,10 +131,10 @@ export default ({ getService }) => {
// The index for the initial migration has not been destroyed...
assert.deepEqual(await fetchDocs({ callCluster, index: `${index}_2` }), [
- { id: 'bar:i', type: 'bar', migrationVersion: { bar: '1.9.0' }, bar: { mynum: 68 } },
- { id: 'bar:o', type: 'bar', migrationVersion: { bar: '1.9.0' }, bar: { mynum: 6 } },
- { id: 'foo:a', type: 'foo', migrationVersion: { foo: '1.0.0' }, foo: { name: 'FOO A' } },
- { id: 'foo:e', type: 'foo', migrationVersion: { foo: '1.0.0' }, foo: { name: 'FOOEY' } },
+ { id: 'bar:i', type: 'bar', migrationVersion: { bar: '1.9.0' }, bar: { mynum: 68 }, references: [] },
+ { id: 'bar:o', type: 'bar', migrationVersion: { bar: '1.9.0' }, bar: { mynum: 6 }, references: [] },
+ { id: 'foo:a', type: 'foo', migrationVersion: { foo: '1.0.0' }, foo: { name: 'FOO A' }, references: [] },
+ { id: 'foo:e', type: 'foo', migrationVersion: { foo: '1.0.0' }, foo: { name: 'FOOEY' }, references: [] },
]);
// The docs were migrated again...
@@ -144,15 +144,17 @@ export default ({ getService }) => {
type: 'bar',
migrationVersion: { bar: '2.3.4' },
bar: { mynum: 68, name: 'NAME i' },
+ references: [],
},
{
id: 'bar:o',
type: 'bar',
migrationVersion: { bar: '2.3.4' },
bar: { mynum: 6, name: 'NAME o' },
+ references: [],
},
- { id: 'foo:a', type: 'foo', migrationVersion: { foo: '2.0.1' }, foo: { name: 'FOO Av2' } },
- { id: 'foo:e', type: 'foo', migrationVersion: { foo: '2.0.1' }, foo: { name: 'FOOEYv2' } },
+ { id: 'foo:a', type: 'foo', migrationVersion: { foo: '2.0.1' }, foo: { name: 'FOO Av2' }, references: [] },
+ { id: 'foo:e', type: 'foo', migrationVersion: { foo: '2.0.1' }, foo: { name: 'FOOEYv2' }, references: [] },
]);
});
@@ -203,7 +205,7 @@ export default ({ getService }) => {
// The docs in the alias have been migrated
assert.deepEqual(await fetchDocs({ callCluster, index }), [
- { id: 'foo:lotr', type: 'foo', migrationVersion: { foo: '1.0.0' }, foo: { name: 'LOTR' } },
+ { id: 'foo:lotr', type: 'foo', migrationVersion: { foo: '1.0.0' }, foo: { name: 'LOTR' }, references: [] },
]);
});
});
diff --git a/test/api_integration/apis/saved_objects/update.js b/test/api_integration/apis/saved_objects/update.js
index e6ca3d0317bf3..a00ab69783cb0 100644
--- a/test/api_integration/apis/saved_objects/update.js
+++ b/test/api_integration/apis/saved_objects/update.js
@@ -51,7 +51,8 @@ export default function ({ getService }) {
version: 2,
attributes: {
title: 'My second favorite vis'
- }
+ },
+ references: [],
});
});
});
diff --git a/x-pack/plugins/graph/index.js b/x-pack/plugins/graph/index.js
index 83134f20d4f82..39dbc148772ec 100644
--- a/x-pack/plugins/graph/index.js
+++ b/x-pack/plugins/graph/index.js
@@ -7,6 +7,7 @@
import { resolve } from 'path';
import Boom from 'boom';
+import migrations from './migrations';
import { initServer } from './server';
import mappings from './mappings.json';
@@ -28,7 +29,8 @@ export function graph(kibana) {
styleSheetPaths: resolve(__dirname, 'public/index.scss'),
hacks: ['plugins/graph/hacks/toggle_app_link_in_nav'],
home: ['plugins/graph/register_feature'],
- mappings
+ mappings,
+ migrations,
},
config(Joi) {
diff --git a/x-pack/plugins/graph/migrations.js b/x-pack/plugins/graph/migrations.js
new file mode 100644
index 0000000000000..d454de49f2b00
--- /dev/null
+++ b/x-pack/plugins/graph/migrations.js
@@ -0,0 +1,41 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { get } from 'lodash';
+
+export default {
+ 'graph-workspace': {
+ '7.0.0': (doc) => {
+ // Set new "references" attribute
+ doc.references = doc.references || [];
+ // Migrate index pattern
+ const wsState = get(doc, 'attributes.wsState');
+ if (typeof wsState !== 'string') {
+ return doc;
+ }
+ let state;
+ try {
+ state = JSON.parse(JSON.parse(wsState));
+ } catch (e) {
+ // Let it go, the data is invalid and we'll leave it as is
+ return doc;
+ }
+ const { indexPattern } = state;
+ if (!indexPattern) {
+ return doc;
+ }
+ state.indexPatternRefName = 'indexPattern_0';
+ delete state.indexPattern;
+ doc.attributes.wsState = JSON.stringify(JSON.stringify(state));
+ doc.references.push({
+ name: 'indexPattern_0',
+ type: 'index-pattern',
+ id: indexPattern,
+ });
+ return doc;
+ }
+ }
+};
diff --git a/x-pack/plugins/graph/migrations.test.js b/x-pack/plugins/graph/migrations.test.js
new file mode 100644
index 0000000000000..93162d94857ce
--- /dev/null
+++ b/x-pack/plugins/graph/migrations.test.js
@@ -0,0 +1,102 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import migrations from './migrations';
+
+describe('graph-workspace', () => {
+ describe('7.0.0', () => {
+ const migration = migrations['graph-workspace']['7.0.0'];
+
+ test('returns doc on empty object', () => {
+ expect(migration({})).toMatchInlineSnapshot(`
+Object {
+ "references": Array [],
+}
+`);
+ });
+
+ test('returns doc when wsState is not a string', () => {
+ const doc = {
+ id: '1',
+ attributes: {
+ wsState: true,
+ },
+ };
+ expect(migration(doc)).toMatchInlineSnapshot(`
+Object {
+ "attributes": Object {
+ "wsState": true,
+ },
+ "id": "1",
+ "references": Array [],
+}
+`);
+ });
+
+ test('returns doc when wsState is not valid JSON', () => {
+ const doc = {
+ id: '1',
+ attributes: {
+ wsState: '123abc',
+ },
+ };
+ expect(migration(doc)).toMatchInlineSnapshot(`
+Object {
+ "attributes": Object {
+ "wsState": "123abc",
+ },
+ "id": "1",
+ "references": Array [],
+}
+`);
+ });
+
+ test('returns doc when "indexPattern" is missing from wsState', () => {
+ const doc = {
+ id: '1',
+ attributes: {
+ wsState: JSON.stringify(JSON.stringify({ foo: true })),
+ },
+ };
+ expect(migration(doc)).toMatchInlineSnapshot(`
+Object {
+ "attributes": Object {
+ "wsState": "\\"{\\\\\\"foo\\\\\\":true}\\"",
+ },
+ "id": "1",
+ "references": Array [],
+}
+`);
+ });
+
+ test('extract "indexPattern" attribute from doc', () => {
+ const doc = {
+ id: '1',
+ attributes: {
+ wsState: JSON.stringify(JSON.stringify({ foo: true, indexPattern: 'pattern*' })),
+ bar: true,
+ },
+ };
+ const migratedDoc = migration(doc);
+ expect(migratedDoc).toMatchInlineSnapshot(`
+Object {
+ "attributes": Object {
+ "bar": true,
+ "wsState": "\\"{\\\\\\"foo\\\\\\":true,\\\\\\"indexPatternRefName\\\\\\":\\\\\\"indexPattern_0\\\\\\"}\\"",
+ },
+ "id": "1",
+ "references": Array [
+ Object {
+ "id": "pattern*",
+ "name": "indexPattern_0",
+ "type": "index-pattern",
+ },
+ ],
+}
+`);
+ });
+ });
+});
diff --git a/x-pack/plugins/graph/public/services/saved_workspace.js b/x-pack/plugins/graph/public/services/saved_workspace.js
index eb0c0e1dd936a..f25743900dab8 100644
--- a/x-pack/plugins/graph/public/services/saved_workspace.js
+++ b/x-pack/plugins/graph/public/services/saved_workspace.js
@@ -7,6 +7,10 @@
import { uiModules } from 'ui/modules';
import { SavedObjectProvider } from 'ui/courier';
import { i18n } from '@kbn/i18n';
+import {
+ extractReferences,
+ injectReferences,
+} from './saved_workspace_references';
const module = uiModules.get('app/dashboard');
@@ -21,6 +25,8 @@ export function SavedWorkspaceProvider(Private) {
type: SavedWorkspace.type,
mapping: SavedWorkspace.mapping,
searchSource: SavedWorkspace.searchsource,
+ extractReferences: extractReferences,
+ injectReferences: injectReferences,
// if this is null/undefined then the SavedObject will be assigned the defaults
id: id,
diff --git a/x-pack/plugins/graph/public/services/saved_workspace_references.js b/x-pack/plugins/graph/public/services/saved_workspace_references.js
new file mode 100644
index 0000000000000..a1b4254685c40
--- /dev/null
+++ b/x-pack/plugins/graph/public/services/saved_workspace_references.js
@@ -0,0 +1,53 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export function extractReferences({ attributes, references = [] }) {
+ // For some reason, wsState comes in stringified 2x
+ const state = JSON.parse(JSON.parse(attributes.wsState));
+ const { indexPattern } = state;
+ if (!indexPattern) {
+ throw new Error('indexPattern attribute is missing in "wsState"');
+ }
+ state.indexPatternRefName = 'indexPattern_0';
+ delete state.indexPattern;
+ return {
+ references: [
+ ...references,
+ {
+ name: 'indexPattern_0',
+ type: 'index-pattern',
+ id: indexPattern,
+ }
+ ],
+ attributes: {
+ ...attributes,
+ wsState: JSON.stringify(JSON.stringify(state)),
+ },
+ };
+}
+
+export function injectReferences(savedObject, references) {
+ // Skip if wsState is missing, at the time of development of this, there is no guarantee each
+ // saved object has wsState.
+ if (typeof savedObject.wsState !== 'string') {
+ return;
+ }
+ // Only need to parse / stringify once here compared to extractReferences
+ const state = JSON.parse(savedObject.wsState);
+ // Like the migration, skip injectReferences if "indexPatternRefName" is missing
+ if (!state.indexPatternRefName) {
+ return;
+ }
+ const indexPatternReference = references.find(reference => reference.name === state.indexPatternRefName);
+ if (!indexPatternReference) {
+ // Throw an error as "indexPatternRefName" means the reference exists within
+ // "references" and in this scenario we have bad data.
+ throw new Error(`Could not find reference "${state.indexPatternRefName}"`);
+ }
+ state.indexPattern = indexPatternReference.id;
+ delete state.indexPatternRefName;
+ savedObject.wsState = JSON.stringify(state);
+}
diff --git a/x-pack/plugins/graph/public/services/saved_workspace_references.test.js b/x-pack/plugins/graph/public/services/saved_workspace_references.test.js
new file mode 100644
index 0000000000000..01eb7f9ead1f0
--- /dev/null
+++ b/x-pack/plugins/graph/public/services/saved_workspace_references.test.js
@@ -0,0 +1,124 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { extractReferences, injectReferences } from './saved_workspace_references';
+
+describe('extractReferences', () => {
+ test('extracts references from wsState', () => {
+ const doc = {
+ id: '1',
+ attributes: {
+ foo: true,
+ wsState: JSON.stringify(
+ JSON.stringify({
+ indexPattern: 'pattern*',
+ bar: true,
+ })
+ ),
+ },
+ };
+ const updatedDoc = extractReferences(doc);
+ expect(updatedDoc).toMatchInlineSnapshot(`
+Object {
+ "attributes": Object {
+ "foo": true,
+ "wsState": "\\"{\\\\\\"bar\\\\\\":true,\\\\\\"indexPatternRefName\\\\\\":\\\\\\"indexPattern_0\\\\\\"}\\"",
+ },
+ "references": Array [
+ Object {
+ "id": "pattern*",
+ "name": "indexPattern_0",
+ "type": "index-pattern",
+ },
+ ],
+}
+`);
+ });
+
+ test('fails when indexPattern is missing from workspace', () => {
+ const doc = {
+ id: '1',
+ attributes: {
+ wsState: JSON.stringify(
+ JSON.stringify({
+ bar: true,
+ })
+ ),
+ },
+ };
+ expect(() => extractReferences(doc)).toThrowErrorMatchingInlineSnapshot(
+ `"indexPattern attribute is missing in \\"wsState\\""`
+ );
+ });
+});
+
+describe('injectReferences', () => {
+ test('injects references into context', () => {
+ const context = {
+ id: '1',
+ foo: true,
+ wsState: JSON.stringify({
+ indexPatternRefName: 'indexPattern_0',
+ bar: true,
+ }),
+ };
+ const references = [
+ {
+ name: 'indexPattern_0',
+ type: 'index-pattern',
+ id: 'pattern*',
+ },
+ ];
+ injectReferences(context, references);
+ expect(context).toMatchInlineSnapshot(`
+Object {
+ "foo": true,
+ "id": "1",
+ "wsState": "{\\"bar\\":true,\\"indexPattern\\":\\"pattern*\\"}",
+}
+`);
+ });
+
+ test('skips when wsState is not a string', () => {
+ const context = {
+ id: '1',
+ foo: true,
+ };
+ injectReferences(context, []);
+ expect(context).toMatchInlineSnapshot(`
+Object {
+ "foo": true,
+ "id": "1",
+}
+`);
+ });
+
+ test('skips when indexPatternRefName is missing wsState', () => {
+ const context = {
+ id: '1',
+ wsState: JSON.stringify({ bar: true }),
+ };
+ injectReferences(context, []);
+ expect(context).toMatchInlineSnapshot(`
+Object {
+ "id": "1",
+ "wsState": "{\\"bar\\":true}",
+}
+`);
+ });
+
+ test(`fails when it can't find the reference in the array`, () => {
+ const context = {
+ id: '1',
+ wsState: JSON.stringify({
+ indexPatternRefName: 'indexPattern_0',
+ }),
+ };
+ expect(() => injectReferences(context, [])).toThrowErrorMatchingInlineSnapshot(
+ `"Could not find reference \\"indexPattern_0\\""`
+ );
+ });
+});
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/apache2/kibana/dashboard/ML-Apache2-Access-Remote-IP-Count-Explorer.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apache2/kibana/dashboard/ML-Apache2-Access-Remote-IP-Count-Explorer.json
index 5fc696c6c702d..24b87e39b35db 100644
--- a/x-pack/plugins/ml/server/models/data_recognizer/modules/apache2/kibana/dashboard/ML-Apache2-Access-Remote-IP-Count-Explorer.json
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/apache2/kibana/dashboard/ML-Apache2-Access-Remote-IP-Count-Explorer.json
@@ -7,6 +7,7 @@
"panelsJSON": "[{\"size_x\":6,\"size_y\":3,\"panelIndex\":1,\"type\":\"visualization\",\"id\":\"ML-Apache2-Access-Remote-IP-Timechart\",\"col\":1,\"row\":1},{\"size_x\":6,\"size_y\":3,\"panelIndex\":2,\"type\":\"visualization\",\"id\":\"ML-Apache2-Access-Response-Code-Timechart\",\"col\":7,\"row\":1},{\"size_x\":6,\"size_y\":3,\"panelIndex\":3,\"type\":\"visualization\",\"id\":\"ML-Apache2-Access-Top-Remote-IPs-Table\",\"col\":1,\"row\":4},{\"size_x\":6,\"size_y\":3,\"panelIndex\":4,\"type\":\"visualization\",\"id\":\"ML-Apache2-Access-Map\",\"col\":7,\"row\":4},{\"size_x\":12,\"size_y\":9,\"panelIndex\":5,\"type\":\"visualization\",\"id\":\"ML-Apache2-Access-Top-URLs-Table\",\"col\":1,\"row\":7}]",
"optionsJSON": "{}",
"version": 1,
+ "migrationVersion": {},
"kibanaSavedObjectMeta": {
"searchSourceJSON": "{\"filter\":[{\"query\":{\"query_string\":{\"analyze_wildcard\":true,\"query\":\"*\"}}}],\"highlightAll\":true,\"version\":true}"
}
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/apache2/kibana/dashboard/ML-Apache2-Remote-IP-URL-Explorer.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apache2/kibana/dashboard/ML-Apache2-Remote-IP-URL-Explorer.json
index b04050ceb6e19..d4ef153201bf2 100644
--- a/x-pack/plugins/ml/server/models/data_recognizer/modules/apache2/kibana/dashboard/ML-Apache2-Remote-IP-URL-Explorer.json
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/apache2/kibana/dashboard/ML-Apache2-Remote-IP-URL-Explorer.json
@@ -7,6 +7,7 @@
"panelsJSON": "[{\"col\":1,\"id\":\"ML-Apache2-Access-Unique-Count-URL-Timechart\",\"panelIndex\":1,\"row\":1,\"size_x\":6,\"size_y\":3,\"type\":\"visualization\"},{\"col\":7,\"id\":\"ML-Apache2-Access-Response-Code-Timechart\",\"panelIndex\":2,\"row\":1,\"size_x\":6,\"size_y\":3,\"type\":\"visualization\"},{\"col\":1,\"id\":\"ML-Apache2-Access-Top-Remote-IPs-Table\",\"panelIndex\":3,\"row\":4,\"size_x\":6,\"size_y\":3,\"type\":\"visualization\"},{\"col\":7,\"id\":\"ML-Apache2-Access-Map\",\"panelIndex\":4,\"row\":4,\"size_x\":6,\"size_y\":3,\"type\":\"visualization\"},{\"size_x\":12,\"size_y\":8,\"panelIndex\":5,\"type\":\"visualization\",\"id\":\"ML-Apache2-Access-Top-URLs-Table\",\"col\":1,\"row\":7}]",
"optionsJSON": "{}",
"version": 1,
+ "migrationVersion": {},
"kibanaSavedObjectMeta": {
"searchSourceJSON": "{\"filter\":[{\"query\":{\"query_string\":{\"analyze_wildcard\":true,\"query\":\"*\"}}}],\"highlightAll\":true,\"version\":true}"
}
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/apache2/kibana/search/ML-Filebeat-Apache2-Access.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apache2/kibana/search/ML-Filebeat-Apache2-Access.json
index edb54752c2ffe..7c6124295aae3 100644
--- a/x-pack/plugins/ml/server/models/data_recognizer/modules/apache2/kibana/search/ML-Filebeat-Apache2-Access.json
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/apache2/kibana/search/ML-Filebeat-Apache2-Access.json
@@ -7,6 +7,7 @@
"description": "Filebeat Apache2 Access Data",
"title": "ML Apache2 Access Data",
"version": 1,
+ "migrationVersion": {},
"kibanaSavedObjectMeta": {
"searchSourceJSON": "{\"index\":\"INDEX_PATTERN_ID\",\"query\":{\"query_string\":{\"query\":\"_exists_:apache2.access\",\"analyze_wildcard\":true}},\"filter\":[],\"highlight\":{\"pre_tags\":[\"@kibana-highlighted-field@\"],\"post_tags\":[\"@/kibana-highlighted-field@\"],\"fields\":{\"*\":{}},\"require_field_match\":false,\"fragment_size\":2147483647}}"
},
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/apache2/kibana/visualization/ML-Apache2-Access-Map.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apache2/kibana/visualization/ML-Apache2-Access-Map.json
index f782f329c037b..c2df807f18985 100644
--- a/x-pack/plugins/ml/server/models/data_recognizer/modules/apache2/kibana/visualization/ML-Apache2-Access-Map.json
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/apache2/kibana/visualization/ML-Apache2-Access-Map.json
@@ -1,11 +1,12 @@
{
- "visState": "{\"aggs\":[{\"enabled\":true,\"id\":\"1\",\"params\":{},\"schema\":\"metric\",\"type\":\"count\"},{\"enabled\":true,\"id\":\"2\",\"params\":{\"autoPrecision\":true,\"field\":\"apache2.access.geoip.location\"},\"schema\":\"segment\",\"type\":\"geohash_grid\"}],\"listeners\":{},\"params\":{\"addTooltip\":true,\"heatBlur\":15,\"heatMaxZoom\":16,\"heatMinOpacity\":0.1,\"heatNormalizeData\":true,\"heatRadius\":25,\"isDesaturated\":true,\"legendPosition\":\"bottomright\",\"mapCenter\":[15,5],\"mapType\":\"Scaled Circle Markers\",\"mapZoom\":2,\"wms\":{\"enabled\":false,\"options\":{\"attribution\":\"Maps provided by USGS\",\"format\":\"image/png\",\"layers\":\"0\",\"styles\":\"\",\"transparent\":true,\"version\":\"1.3.0\"},\"url\":\"https://basemap.nationalmap.gov/arcgis/services/USGSTopo/MapServer/WMSServer\"}},\"title\":\"ML Apache2 Access Map\",\"type\":\"tile_map\"}",
- "description": "",
- "title": "ML Apache2 Access Map",
- "uiStateJSON": "{\n \"mapCenter\": [\n 12.039320557540572,\n -0.17578125\n ]\n}",
- "version": 1,
- "savedSearchId": "ML-Filebeat-Apache2-Access",
+ "visState": "{\"aggs\":[{\"enabled\":true,\"id\":\"1\",\"params\":{},\"schema\":\"metric\",\"type\":\"count\"},{\"enabled\":true,\"id\":\"2\",\"params\":{\"autoPrecision\":true,\"field\":\"apache2.access.geoip.location\"},\"schema\":\"segment\",\"type\":\"geohash_grid\"}],\"listeners\":{},\"params\":{\"addTooltip\":true,\"heatBlur\":15,\"heatMaxZoom\":16,\"heatMinOpacity\":0.1,\"heatNormalizeData\":true,\"heatRadius\":25,\"isDesaturated\":true,\"legendPosition\":\"bottomright\",\"mapCenter\":[15,5],\"mapType\":\"Scaled Circle Markers\",\"mapZoom\":2,\"wms\":{\"enabled\":false,\"options\":{\"attribution\":\"Maps provided by USGS\",\"format\":\"image/png\",\"layers\":\"0\",\"styles\":\"\",\"transparent\":true,\"version\":\"1.3.0\"},\"url\":\"https://basemap.nationalmap.gov/arcgis/services/USGSTopo/MapServer/WMSServer\"}},\"title\":\"ML Apache2 Access Map\",\"type\":\"tile_map\"}",
+ "description": "",
+ "title": "ML Apache2 Access Map",
+ "uiStateJSON": "{\n \"mapCenter\": [\n 12.039320557540572,\n -0.17578125\n ]\n}",
+ "version": 1,
+ "migrationVersion": {},
+ "savedSearchId": "ML-Filebeat-Apache2-Access",
"kibanaSavedObjectMeta": {
"searchSourceJSON": "{\"filter\":[]}"
}
-}
\ No newline at end of file
+}
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/apache2/kibana/visualization/ML-Apache2-Access-Remote-IP-Timechart.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apache2/kibana/visualization/ML-Apache2-Access-Remote-IP-Timechart.json
index dcceff40a2c0f..0789ee1e03f6f 100644
--- a/x-pack/plugins/ml/server/models/data_recognizer/modules/apache2/kibana/visualization/ML-Apache2-Access-Remote-IP-Timechart.json
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/apache2/kibana/visualization/ML-Apache2-Access-Remote-IP-Timechart.json
@@ -1,11 +1,12 @@
{
- "visState": "{\"title\":\"ML Apache2 Access Remote IP Timechart\",\"type\":\"area\",\"params\":{\"addLegend\":true,\"addTimeMarker\":false,\"addTooltip\":true,\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"labels\":{\"show\":true,\"truncate\":100},\"position\":\"bottom\",\"scale\":{\"type\":\"linear\"},\"show\":true,\"style\":{},\"title\":{\"text\":\"@timestamp per 5 minutes\"},\"type\":\"category\"}],\"defaultYExtents\":false,\"drawLinesBetweenPoints\":true,\"grid\":{\"categoryLines\":false,\"style\":{\"color\":\"#eee\"}},\"interpolate\":\"linear\",\"legendPosition\":\"right\",\"radiusRatio\":9,\"scale\":\"linear\",\"seriesParams\":[{\"data\":{\"id\":\"1\",\"label\":\"Count\"},\"drawLinesBetweenPoints\":true,\"interpolate\":\"linear\",\"mode\":\"stacked\",\"show\":\"true\",\"showCircles\":true,\"type\":\"area\",\"valueAxis\":\"ValueAxis-1\"}],\"setYExtents\":false,\"showCircles\":true,\"times\":[],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"labels\":{\"filter\":false,\"rotate\":0,\"show\":true,\"truncate\":100},\"name\":\"LeftAxis-1\",\"position\":\"left\",\"scale\":{\"mode\":\"normal\",\"type\":\"linear\"},\"show\":true,\"style\":{},\"title\":{},\"type\":\"value\"}]},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"interval\":\"auto\",\"customInterval\":\"2h\",\"min_doc_count\":1,\"extended_bounds\":{}}},{\"id\":\"3\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"group\",\"params\":{\"field\":\"apache2.access.remote_ip\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\"}}],\"listeners\":{}}",
- "description": "",
- "title": "ML Apache2 Access Remote IP Timechart",
- "uiStateJSON": "{\"vis\":{\"legendOpen\":false}}",
- "version": 1,
- "savedSearchId": "ML-Filebeat-Apache2-Access",
+ "visState": "{\"title\":\"ML Apache2 Access Remote IP Timechart\",\"type\":\"area\",\"params\":{\"addLegend\":true,\"addTimeMarker\":false,\"addTooltip\":true,\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"labels\":{\"show\":true,\"truncate\":100},\"position\":\"bottom\",\"scale\":{\"type\":\"linear\"},\"show\":true,\"style\":{},\"title\":{\"text\":\"@timestamp per 5 minutes\"},\"type\":\"category\"}],\"defaultYExtents\":false,\"drawLinesBetweenPoints\":true,\"grid\":{\"categoryLines\":false,\"style\":{\"color\":\"#eee\"}},\"interpolate\":\"linear\",\"legendPosition\":\"right\",\"radiusRatio\":9,\"scale\":\"linear\",\"seriesParams\":[{\"data\":{\"id\":\"1\",\"label\":\"Count\"},\"drawLinesBetweenPoints\":true,\"interpolate\":\"linear\",\"mode\":\"stacked\",\"show\":\"true\",\"showCircles\":true,\"type\":\"area\",\"valueAxis\":\"ValueAxis-1\"}],\"setYExtents\":false,\"showCircles\":true,\"times\":[],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"labels\":{\"filter\":false,\"rotate\":0,\"show\":true,\"truncate\":100},\"name\":\"LeftAxis-1\",\"position\":\"left\",\"scale\":{\"mode\":\"normal\",\"type\":\"linear\"},\"show\":true,\"style\":{},\"title\":{},\"type\":\"value\"}]},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"interval\":\"auto\",\"customInterval\":\"2h\",\"min_doc_count\":1,\"extended_bounds\":{}}},{\"id\":\"3\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"group\",\"params\":{\"field\":\"apache2.access.remote_ip\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\"}}],\"listeners\":{}}",
+ "description": "",
+ "title": "ML Apache2 Access Remote IP Timechart",
+ "uiStateJSON": "{\"vis\":{\"legendOpen\":false}}",
+ "version": 1,
+ "migrationVersion": {},
+ "savedSearchId": "ML-Filebeat-Apache2-Access",
"kibanaSavedObjectMeta": {
"searchSourceJSON": "{}"
}
-}
\ No newline at end of file
+}
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/apache2/kibana/visualization/ML-Apache2-Access-Response-Code-Timechart.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apache2/kibana/visualization/ML-Apache2-Access-Response-Code-Timechart.json
index f52019014b138..bc0a22685d5cf 100644
--- a/x-pack/plugins/ml/server/models/data_recognizer/modules/apache2/kibana/visualization/ML-Apache2-Access-Response-Code-Timechart.json
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/apache2/kibana/visualization/ML-Apache2-Access-Response-Code-Timechart.json
@@ -1,11 +1,12 @@
{
- "visState": "{\"title\":\"ML Apache2 Access Response Code Timechart\",\"type\":\"histogram\",\"params\":{\"shareYAxis\":true,\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"scale\":\"linear\",\"mode\":\"stacked\",\"times\":[],\"addTimeMarker\":false,\"defaultYExtents\":false,\"setYExtents\":false,\"yAxis\":{}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"interval\":\"auto\",\"customInterval\":\"2h\",\"min_doc_count\":1,\"extended_bounds\":{}}},{\"id\":\"3\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"group\",\"params\":{\"field\":\"apache2.access.response_code\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\"}}],\"listeners\":{}}",
- "description": "",
- "title": "ML Apache2 Access Response Code Timechart",
- "uiStateJSON": "{\n \"vis\": {\n \"colors\": {\n \"200\": \"#7EB26D\",\n \"404\": \"#614D93\"\n }\n }\n}",
- "version": 1,
- "savedSearchId": "ML-Filebeat-Apache2-Access",
+ "visState": "{\"title\":\"ML Apache2 Access Response Code Timechart\",\"type\":\"histogram\",\"params\":{\"shareYAxis\":true,\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"scale\":\"linear\",\"mode\":\"stacked\",\"times\":[],\"addTimeMarker\":false,\"defaultYExtents\":false,\"setYExtents\":false,\"yAxis\":{}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"interval\":\"auto\",\"customInterval\":\"2h\",\"min_doc_count\":1,\"extended_bounds\":{}}},{\"id\":\"3\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"group\",\"params\":{\"field\":\"apache2.access.response_code\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\"}}],\"listeners\":{}}",
+ "description": "",
+ "title": "ML Apache2 Access Response Code Timechart",
+ "uiStateJSON": "{\n \"vis\": {\n \"colors\": {\n \"200\": \"#7EB26D\",\n \"404\": \"#614D93\"\n }\n }\n}",
+ "version": 1,
+ "migrationVersion": {},
+ "savedSearchId": "ML-Filebeat-Apache2-Access",
"kibanaSavedObjectMeta": {
"searchSourceJSON": "{\"filter\":[]}"
}
-}
\ No newline at end of file
+}
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/apache2/kibana/visualization/ML-Apache2-Access-Top-Remote-IPs-Table.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apache2/kibana/visualization/ML-Apache2-Access-Top-Remote-IPs-Table.json
index 0c49d9de46001..38943fd9ee6ac 100644
--- a/x-pack/plugins/ml/server/models/data_recognizer/modules/apache2/kibana/visualization/ML-Apache2-Access-Top-Remote-IPs-Table.json
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/apache2/kibana/visualization/ML-Apache2-Access-Top-Remote-IPs-Table.json
@@ -1,11 +1,12 @@
{
- "visState": "{\"title\":\"ML Apache2 Access Top Remote IPs Table\",\"type\":\"table\",\"params\":{\"perPage\":10,\"showPartialRows\":false,\"showMetricsAtAllLevels\":false,\"sort\":{\"columnIndex\":null,\"direction\":null},\"showTotal\":false,\"totalFunc\":\"sum\"},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"bucket\",\"params\":{\"field\":\"apache2.access.remote_ip\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\"}}],\"listeners\":{}}",
- "description": "",
- "title": "ML Apache2 Access Top Remote IPs Table",
- "uiStateJSON": "{\"vis\":{\"params\":{\"sort\":{\"columnIndex\":null,\"direction\":null}}}}",
- "version": 1,
- "savedSearchId": "ML-Filebeat-Apache2-Access",
+ "visState": "{\"title\":\"ML Apache2 Access Top Remote IPs Table\",\"type\":\"table\",\"params\":{\"perPage\":10,\"showPartialRows\":false,\"showMetricsAtAllLevels\":false,\"sort\":{\"columnIndex\":null,\"direction\":null},\"showTotal\":false,\"totalFunc\":\"sum\"},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"bucket\",\"params\":{\"field\":\"apache2.access.remote_ip\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\"}}],\"listeners\":{}}",
+ "description": "",
+ "title": "ML Apache2 Access Top Remote IPs Table",
+ "uiStateJSON": "{\"vis\":{\"params\":{\"sort\":{\"columnIndex\":null,\"direction\":null}}}}",
+ "version": 1,
+ "migrationVersion": {},
+ "savedSearchId": "ML-Filebeat-Apache2-Access",
"kibanaSavedObjectMeta": {
"searchSourceJSON": "{}"
}
-}
\ No newline at end of file
+}
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/apache2/kibana/visualization/ML-Apache2-Access-Top-URLs-Table.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apache2/kibana/visualization/ML-Apache2-Access-Top-URLs-Table.json
index ab7760feff999..406c314787c72 100644
--- a/x-pack/plugins/ml/server/models/data_recognizer/modules/apache2/kibana/visualization/ML-Apache2-Access-Top-URLs-Table.json
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/apache2/kibana/visualization/ML-Apache2-Access-Top-URLs-Table.json
@@ -1,11 +1,12 @@
{
- "visState": "{\"title\":\"ML Apache2 Access Top URLs Table\",\"type\":\"table\",\"params\":{\"perPage\":100,\"showPartialRows\":false,\"showMetricsAtAllLevels\":false,\"sort\":{\"columnIndex\":null,\"direction\":null},\"showTotal\":false,\"totalFunc\":\"sum\"},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"bucket\",\"params\":{\"field\":\"apache2.access.url\",\"size\":1000,\"order\":\"desc\",\"orderBy\":\"1\"}}],\"listeners\":{}}",
- "description": "",
- "title": "ML Apache2 Access Top URLs Table",
- "uiStateJSON": "{\"vis\":{\"params\":{\"sort\":{\"columnIndex\":null,\"direction\":null}}}}",
- "version": 1,
- "savedSearchId": "ML-Filebeat-Apache2-Access",
+ "visState": "{\"title\":\"ML Apache2 Access Top URLs Table\",\"type\":\"table\",\"params\":{\"perPage\":100,\"showPartialRows\":false,\"showMetricsAtAllLevels\":false,\"sort\":{\"columnIndex\":null,\"direction\":null},\"showTotal\":false,\"totalFunc\":\"sum\"},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"bucket\",\"params\":{\"field\":\"apache2.access.url\",\"size\":1000,\"order\":\"desc\",\"orderBy\":\"1\"}}],\"listeners\":{}}",
+ "description": "",
+ "title": "ML Apache2 Access Top URLs Table",
+ "uiStateJSON": "{\"vis\":{\"params\":{\"sort\":{\"columnIndex\":null,\"direction\":null}}}}",
+ "version": 1,
+ "migrationVersion": {},
+ "savedSearchId": "ML-Filebeat-Apache2-Access",
"kibanaSavedObjectMeta": {
"searchSourceJSON": "{}"
}
-}
\ No newline at end of file
+}
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/apache2/kibana/visualization/ML-Apache2-Access-Unique-Count-URL-Timechart.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apache2/kibana/visualization/ML-Apache2-Access-Unique-Count-URL-Timechart.json
index c6ce4fcf741eb..54d324434925c 100644
--- a/x-pack/plugins/ml/server/models/data_recognizer/modules/apache2/kibana/visualization/ML-Apache2-Access-Unique-Count-URL-Timechart.json
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/apache2/kibana/visualization/ML-Apache2-Access-Unique-Count-URL-Timechart.json
@@ -1,11 +1,12 @@
{
- "visState": "{\"title\":\"ML Apache2 Access Unique Count URL Timechart\",\"type\":\"line\",\"params\":{\"grid\":{\"categoryLines\":false,\"style\":{\"color\":\"#eee\"}},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"truncate\":100},\"title\":{\"text\":\"@timestamp per day\"}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Unique count of apache2.access.url\"}}],\"seriesParams\":[{\"show\":true,\"mode\":\"normal\",\"type\":\"line\",\"drawLinesBetweenPoints\":true,\"showCircles\":true,\"interpolate\":\"linear\",\"lineWidth\":2,\"data\":{\"id\":\"1\",\"label\":\"Unique count of apache2.access.url\"},\"valueAxis\":\"ValueAxis-1\"}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"showCircles\":true,\"interpolate\":\"linear\",\"scale\":\"linear\",\"drawLinesBetweenPoints\":true,\"radiusRatio\":9,\"times\":[],\"addTimeMarker\":false,\"defaultYExtents\":false,\"setYExtents\":false},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"cardinality\",\"schema\":\"metric\",\"params\":{\"field\":\"apache2.access.url\"}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"interval\":\"auto\",\"customInterval\":\"2h\",\"min_doc_count\":1,\"extended_bounds\":{}}}],\"listeners\":{}}",
- "description": "",
- "title": "ML Apache2 Access Unique Count URL Timechart",
- "uiStateJSON": "{}",
- "version": 1,
- "savedSearchId": "ML-Filebeat-Apache2-Access",
+ "visState": "{\"title\":\"ML Apache2 Access Unique Count URL Timechart\",\"type\":\"line\",\"params\":{\"grid\":{\"categoryLines\":false,\"style\":{\"color\":\"#eee\"}},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"truncate\":100},\"title\":{\"text\":\"@timestamp per day\"}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Unique count of apache2.access.url\"}}],\"seriesParams\":[{\"show\":true,\"mode\":\"normal\",\"type\":\"line\",\"drawLinesBetweenPoints\":true,\"showCircles\":true,\"interpolate\":\"linear\",\"lineWidth\":2,\"data\":{\"id\":\"1\",\"label\":\"Unique count of apache2.access.url\"},\"valueAxis\":\"ValueAxis-1\"}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"showCircles\":true,\"interpolate\":\"linear\",\"scale\":\"linear\",\"drawLinesBetweenPoints\":true,\"radiusRatio\":9,\"times\":[],\"addTimeMarker\":false,\"defaultYExtents\":false,\"setYExtents\":false},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"cardinality\",\"schema\":\"metric\",\"params\":{\"field\":\"apache2.access.url\"}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"interval\":\"auto\",\"customInterval\":\"2h\",\"min_doc_count\":1,\"extended_bounds\":{}}}],\"listeners\":{}}",
+ "description": "",
+ "title": "ML Apache2 Access Unique Count URL Timechart",
+ "uiStateJSON": "{}",
+ "version": 1,
+ "migrationVersion": {},
+ "savedSearchId": "ML-Filebeat-Apache2-Access",
"kibanaSavedObjectMeta": {
"searchSourceJSON": "{}"
}
-}
\ No newline at end of file
+}
diff --git a/x-pack/plugins/rollup/server/usage/collector.js b/x-pack/plugins/rollup/server/usage/collector.js
index 4cb9fba66ca67..d017740def9e3 100644
--- a/x-pack/plugins/rollup/server/usage/collector.js
+++ b/x-pack/plugins/rollup/server/usage/collector.js
@@ -102,8 +102,9 @@ async function fetchRollupVisualizations(kibanaIndex, callCluster, rollupIndexPa
index: kibanaIndex,
ignoreUnavailable: true,
filterPath: [
- 'hits.hits._source.visualization.savedSearchId',
+ 'hits.hits._source.visualization.savedSearchRefName',
'hits.hits._source.visualization.kibanaSavedObjectMeta',
+ 'hits.hits._source.references',
],
body: {
query: {
@@ -128,19 +129,21 @@ async function fetchRollupVisualizations(kibanaIndex, callCluster, rollupIndexPa
const {
_source: {
visualization: {
- savedSearchId,
+ savedSearchRefName,
kibanaSavedObjectMeta: {
searchSourceJSON,
},
},
+ references = [],
},
} = visualization;
const searchSource = JSON.parse(searchSourceJSON);
- if (savedSearchId) {
+ if (savedSearchRefName) {
// This visualization depends upon a saved search.
- if (rollupSavedSearchesToFlagMap[savedSearchId]) {
+ const savedSearch = references.find(ref => ref.name === savedSearchRefName);
+ if (rollupSavedSearchesToFlagMap[savedSearch.id]) {
rollupVisualizations++;
rollupVisualizationsFromSavedSearches++;
}
diff --git a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.ts b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.ts
index 5e86cd4f08c5e..9a3203305b59d 100644
--- a/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.ts
+++ b/x-pack/plugins/spaces/server/lib/saved_objects_client/spaces_saved_objects_client.ts
@@ -149,6 +149,7 @@ export class SpacesSavedObjectsClient implements SavedObjectsClient {
* @property {string} [options.sortOrder]
* @property {Array} [options.fields]
* @property {string} [options.namespace]
+ * @property {object} [options.hasReference] - { type, id }
* @returns {promise} - { saved_objects: [{ id, type, version, attributes }], total, per_page, page }
*/
public async find(options: FindOptions = {}) {
diff --git a/x-pack/plugins/spaces/server/lib/space_request_interceptors.test.ts b/x-pack/plugins/spaces/server/lib/space_request_interceptors.test.ts
index 9d9ad1417a3e9..7604cbc06f712 100644
--- a/x-pack/plugins/spaces/server/lib/space_request_interceptors.test.ts
+++ b/x-pack/plugins/spaces/server/lib/space_request_interceptors.test.ts
@@ -229,6 +229,7 @@ describe('interceptors', () => {
attributes: {
name: 'a space',
},
+ references: [],
},
];
@@ -265,6 +266,7 @@ describe('interceptors', () => {
attributes: {
name: 'a space',
},
+ references: [],
},
];
@@ -302,6 +304,7 @@ describe('interceptors', () => {
attributes: {
name: 'a space',
},
+ references: [],
},
];
@@ -348,6 +351,7 @@ describe('interceptors', () => {
attributes: {
name: 'Default Space',
},
+ references: [],
},
];
@@ -379,6 +383,7 @@ describe('interceptors', () => {
attributes: {
name: 'a space',
},
+ references: [],
},
{
id: 'b-space',
@@ -386,6 +391,7 @@ describe('interceptors', () => {
attributes: {
name: 'b space',
},
+ references: [],
},
];
diff --git a/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json b/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json
index 6bf3413f3797d..368384479bc30 100644
--- a/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json
+++ b/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json
@@ -195,7 +195,7 @@
"description": "",
"version": 1,
"kibanaSavedObjectMeta": {
- "searchSourceJSON": "{\"index\":\"91200a00-9efd-11e7-acb3-3dab96693fab\",\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"}}"
+ "searchSourceJSON": "{\"index\":\"space_1-91200a00-9efd-11e7-acb3-3dab96693fab\",\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"}}"
}
}
}
@@ -216,7 +216,7 @@
"title": "Requests",
"hits": 0,
"description": "",
- "panelsJSON": "[{\"size_x\":6,\"size_y\":3,\"panelIndex\":1,\"type\":\"visualization\",\"id\":\"dd7caf20-9efd-11e7-acb3-3dab96693fab\",\"col\":1,\"row\":1}]",
+ "panelsJSON": "[{\"size_x\":6,\"size_y\":3,\"panelIndex\":1,\"type\":\"visualization\",\"id\":\"space_1-dd7caf20-9efd-11e7-acb3-3dab96693fab\",\"col\":1,\"row\":1}]",
"optionsJSON": "{}",
"uiStateJSON": "{}",
"version": 1,
@@ -291,7 +291,7 @@
"description": "",
"version": 1,
"kibanaSavedObjectMeta": {
- "searchSourceJSON": "{\"index\":\"91200a00-9efd-11e7-acb3-3dab96693fab\",\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"}}"
+ "searchSourceJSON": "{\"index\":\"space_2-91200a00-9efd-11e7-acb3-3dab96693fab\",\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"}}"
}
}
}
@@ -312,7 +312,7 @@
"title": "Requests",
"hits": 0,
"description": "",
- "panelsJSON": "[{\"size_x\":6,\"size_y\":3,\"panelIndex\":1,\"type\":\"visualization\",\"id\":\"dd7caf20-9efd-11e7-acb3-3dab96693fab\",\"col\":1,\"row\":1}]",
+ "panelsJSON": "[{\"size_x\":6,\"size_y\":3,\"panelIndex\":1,\"type\":\"visualization\",\"id\":\"space_2-dd7caf20-9efd-11e7-acb3-3dab96693fab\",\"col\":1,\"row\":1}]",
"optionsJSON": "{}",
"uiStateJSON": "{}",
"version": 1,
diff --git a/x-pack/test/saved_object_api_integration/common/suites/bulk_create.ts b/x-pack/test/saved_object_api_integration/common/suites/bulk_create.ts
index 0d9e800c64003..6dcaae760a58d 100644
--- a/x-pack/test/saved_object_api_integration/common/suites/bulk_create.ts
+++ b/x-pack/test/saved_object_api_integration/common/suites/bulk_create.ts
@@ -88,6 +88,7 @@ export function bulkCreateTestSuiteFactory(es: any, esArchiver: any, supertest:
attributes: {
title: 'A great new dashboard',
},
+ references: [],
},
{
type: 'globaltype',
@@ -97,6 +98,7 @@ export function bulkCreateTestSuiteFactory(es: any, esArchiver: any, supertest:
attributes: {
name: 'A new globaltype object',
},
+ references: [],
},
{
type: 'globaltype',
diff --git a/x-pack/test/saved_object_api_integration/common/suites/bulk_get.ts b/x-pack/test/saved_object_api_integration/common/suites/bulk_get.ts
index fdffe545f3416..e82ea7b856a8c 100644
--- a/x-pack/test/saved_object_api_integration/common/suites/bulk_get.ts
+++ b/x-pack/test/saved_object_api_integration/common/suites/bulk_get.ts
@@ -69,6 +69,7 @@ export function bulkGetTestSuiteFactory(esArchiver: any, supertest: SuperTest)
attributes: {
name: 'My favorite global object',
},
+ references: [],
},
],
});
@@ -99,6 +100,7 @@ export function findTestSuiteFactory(esArchiver: any, supertest: SuperTest)
attributes: {
title: 'Count of requests',
},
+ references: [],
},
],
});
diff --git a/x-pack/test/saved_object_api_integration/common/suites/get.ts b/x-pack/test/saved_object_api_integration/common/suites/get.ts
index 5bf2385544a7d..1cc36b411c61a 100644
--- a/x-pack/test/saved_object_api_integration/common/suites/get.ts
+++ b/x-pack/test/saved_object_api_integration/common/suites/get.ts
@@ -69,6 +69,7 @@ export function getTestSuiteFactory(esArchiver: any, supertest: SuperTest)
attributes: {
name: 'My favorite global object',
},
+ references: [],
});
};
@@ -108,6 +109,13 @@ export function getTestSuiteFactory(esArchiver: any, supertest: SuperTest)
uiStateJSON: resp.body.attributes.uiStateJSON,
kibanaSavedObjectMeta: resp.body.attributes.kibanaSavedObjectMeta,
},
+ references: [
+ {
+ name: 'kibanaSavedObjectMeta.searchSourceJSON.index',
+ type: 'index-pattern',
+ id: `${getIdPrefix(spaceId)}91200a00-9efd-11e7-acb3-3dab96693fab`,
+ },
+ ],
});
};
diff --git a/x-pack/test/saved_object_api_integration/common/suites/update.ts b/x-pack/test/saved_object_api_integration/common/suites/update.ts
index f936567200f54..5d496ba58bbba 100644
--- a/x-pack/test/saved_object_api_integration/common/suites/update.ts
+++ b/x-pack/test/saved_object_api_integration/common/suites/update.ts
@@ -78,6 +78,7 @@ export function updateTestSuiteFactory(esArchiver: any, supertest: SuperTest