mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
Add new "references" attribute to saved objects for relationships (#28199)
* Add new references attribute to saved objects * Add dual support for dashboard export API * Use new relationships API supporting legacy relationships extraction * Code cleanup * Fix style and CI error * Add missing spaces test for findRelationships * Convert collect_references_deep to typescript * Add missing trailing commas * Fix broken test by making saved object API consistently return references * Fix broken api integration tests * Add comment about the two TS types for saved object * Only return title from the attributes returned in findRelationships * Fix broken test * Add missing security tests * Drop filterTypes support * Implement references to search, dashboard, visualization, graph * Add index pattern migration to dashboards * Add references mapping to dashboard mppings.json * Remove findRelationships from repository and into it's own function / file * Apply PR feedback pt1 * Fix some failing tests * Remove error throwing in migrations * Add references to edit saved object screen * Pass types to findRelationships * [ftr] restore snapshots from master, rely on migrations to add references * [security] remove `find_relationships` action * remove data set modifications * [security/savedObjectsClient] remove _getAuthorizedTypes method * fix security & spaces tests to consider references and migrationVersion * Add space id prefixes to es_archiver/saved_objects/spaces/data.json * Rename referenced attributes to have a suffix of RefName * Fix length check in scenario references doesn't exist * Add test for inject references to not be called when references array is empty or missing * some code cleanup * Make migrations run on machine learning data files, fix rollup filterPath for savedSearchRefName * fix broken test * Fix collector.js to include references in elasticsearch response * code cleanup pt2 * add some more tests * fix broken tests * updated documentation on referencedBy option for saved object client find function * Move visualization migrations into kibana plugin * Update docs with better description on references * Apply PR feedback * Fix merge * fix tests I broke adressing PR feedback * PR feedback pt2
This commit is contained in:
parent
410c094547
commit
1b0f595f01
99 changed files with 3123 additions and 1171 deletions
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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]
|
||||
==============================================
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -42,7 +42,7 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"savedSearchId": {
|
||||
"savedSearchRefName": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"title": {
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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",
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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\\""`
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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');
|
||||
})
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
|
|
|
@ -148,7 +148,7 @@ class RelationshipsUI extends Component {
|
|||
/>);
|
||||
break;
|
||||
case 'search':
|
||||
if (type === 'visualizations') {
|
||||
if (type === 'visualization') {
|
||||
calloutText = (<FormattedMessage
|
||||
id="kbn.management.objects.objectsTable.relationships.search.visualizations.calloutText"
|
||||
defaultMessage="Here are some visualizations that use this saved search. If
|
||||
|
@ -168,22 +168,44 @@ class RelationshipsUI extends Component {
|
|||
}
|
||||
break;
|
||||
case 'visualization':
|
||||
calloutText = (<FormattedMessage
|
||||
id="kbn.management.objects.objectsTable.relationships.visualization.calloutText"
|
||||
defaultMessage="Here are some dashboards which contain this visualization. If
|
||||
you delete this visualization, these dashboards will no longer
|
||||
show them."
|
||||
/>);
|
||||
if (type === 'index-pattern') {
|
||||
calloutColor = 'success';
|
||||
calloutTitle = (<FormattedMessage
|
||||
id="kbn.management.objects.objectsTable.relationships.visualization.indexPattern.calloutTitle"
|
||||
defaultMessage="Index Pattern"
|
||||
/>);
|
||||
calloutText = (<FormattedMessage
|
||||
id="kbn.management.objects.objectsTable.relationships.visualization.indexPattern.calloutText"
|
||||
defaultMessage="Here is the index pattern tied to this visualization."
|
||||
/>);
|
||||
} else if (type === 'search') {
|
||||
calloutColor = 'success';
|
||||
calloutTitle = (<FormattedMessage
|
||||
id="kbn.management.objects.objectsTable.relationships.visualization.search.calloutTitle"
|
||||
defaultMessage="Saved Search"
|
||||
/>);
|
||||
calloutText = (<FormattedMessage
|
||||
id="kbn.management.objects.objectsTable.relationships.visualization.search.calloutText"
|
||||
defaultMessage="Here is the saved search tied to this visualization."
|
||||
/>);
|
||||
} else {
|
||||
calloutText = (<FormattedMessage
|
||||
id="kbn.management.objects.objectsTable.relationships.visualization.calloutText"
|
||||
defaultMessage="Here are some dashboards which contain this visualization. If
|
||||
you delete this visualization, these dashboards will no longer
|
||||
show them."
|
||||
/>);
|
||||
}
|
||||
break;
|
||||
case 'index-pattern':
|
||||
if (type === 'visualizations') {
|
||||
if (type === 'visualization') {
|
||||
calloutText = (<FormattedMessage
|
||||
id="kbn.management.objects.objectsTable.relationships.indexPattern.visualizations.calloutText"
|
||||
defaultMessage="Here are some visualizations that use this index pattern. If
|
||||
you delete this index pattern, these visualizations will not
|
||||
longer work properly."
|
||||
/>);
|
||||
} else if (type === 'searches') {
|
||||
} else if (type === 'search') {
|
||||
calloutText = (<FormattedMessage
|
||||
id="kbn.management.objects.objectsTable.relationships.indexPattern.searches.calloutText"
|
||||
defaultMessage="Here are some saved searches that use this index pattern. If
|
||||
|
|
|
@ -82,7 +82,10 @@ async function importIndexPattern(doc, indexPatterns, overwriteAll) {
|
|||
}
|
||||
|
||||
async function importDocument(obj, doc, overwriteAll) {
|
||||
await obj.applyESResp(doc);
|
||||
await obj.applyESResp({
|
||||
references: doc._references || [],
|
||||
...doc,
|
||||
});
|
||||
return await obj.save({ confirmOverwrite: !overwriteAll });
|
||||
}
|
||||
|
||||
|
|
|
@ -27,6 +27,7 @@ export async function retrieveAndExportDocs(objs, savedObjectsClient) {
|
|||
_type: obj.type,
|
||||
_source: obj.attributes,
|
||||
_migrationVersion: obj.migrationVersion,
|
||||
_references: obj.references,
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
@ -31,6 +31,10 @@ import { updateOldState } from 'ui/vis/vis_update_state';
|
|||
import { VisualizeConstants } from '../visualize_constants';
|
||||
import { createLegacyClass } from 'ui/utils/legacy_class';
|
||||
import { SavedObjectProvider } from 'ui/courier';
|
||||
import {
|
||||
extractReferences,
|
||||
injectReferences,
|
||||
} from './saved_visualization_references';
|
||||
|
||||
uiModules
|
||||
.get('app/visualize')
|
||||
|
@ -47,6 +51,8 @@ uiModules
|
|||
type: SavedVis.type,
|
||||
mapping: SavedVis.mapping,
|
||||
searchSource: SavedVis.searchSource,
|
||||
extractReferences: extractReferences,
|
||||
injectReferences: injectReferences,
|
||||
|
||||
id: opts.id,
|
||||
indexPattern: opts.indexPattern,
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* 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 = [] }) {
|
||||
if (!attributes.savedSearchId) {
|
||||
return { attributes, references };
|
||||
}
|
||||
return {
|
||||
references: [
|
||||
...references,
|
||||
{
|
||||
type: 'search',
|
||||
name: 'search_0',
|
||||
id: attributes.savedSearchId,
|
||||
},
|
||||
],
|
||||
attributes: {
|
||||
...attributes,
|
||||
savedSearchId: undefined,
|
||||
savedSearchRefName: 'search_0',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function injectReferences(savedObject, references) {
|
||||
if (!savedObject.savedSearchRefName) {
|
||||
return;
|
||||
}
|
||||
const reference = references.find(reference => reference.name === savedObject.savedSearchRefName);
|
||||
if (!reference) {
|
||||
throw new Error(`Could not find reference "${savedObject.savedSearchRefName}"`);
|
||||
}
|
||||
savedObject.savedSearchId = reference.id;
|
||||
delete savedObject.savedSearchRefName;
|
||||
}
|
|
@ -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\\""`
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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 {
|
||||
id: '1',
|
||||
attributes: {
|
||||
kibanaSavedObjectMeta: {
|
||||
searchSourceJSON: JSON.stringify({
|
||||
index: 'index-pattern:1',
|
||||
}),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
id: 'index-pattern:1',
|
||||
attributes: {
|
||||
title: 'My Index Pattern',
|
||||
get: () => ({
|
||||
id: '1',
|
||||
type: 'search',
|
||||
attributes: {
|
||||
kibanaSavedObjectMeta: {
|
||||
searchSourceJSON: JSON.stringify({
|
||||
indexRefName: 'kibanaSavedObjectMeta.searchSourceJSON.index',
|
||||
}),
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
references: [{
|
||||
name: 'kibanaSavedObjectMeta.searchSourceJSON.index',
|
||||
type: 'index-pattern',
|
||||
id: '1',
|
||||
}],
|
||||
}),
|
||||
bulkGet: () => ({
|
||||
saved_objects: [
|
||||
{
|
||||
id: '1',
|
||||
type: 'index-pattern',
|
||||
attributes: {
|
||||
title: 'My Index Pattern',
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
find: () => ({
|
||||
saved_objects: [
|
||||
{
|
||||
id: '1',
|
||||
type: 'visualization',
|
||||
attributes: {
|
||||
title: 'Foo',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: 'visualization',
|
||||
attributes: {
|
||||
title: 'Bar',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
type: 'visualization',
|
||||
attributes: {
|
||||
title: 'FooBar',
|
||||
},
|
||||
|
@ -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({});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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' },
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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
|
||||
]);
|
||||
});
|
||||
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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' }
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
}, []);
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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]);
|
||||
}
|
|
@ -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",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -17,166 +17,45 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
async function findDashboardRelationships(id, size, savedObjectsClient) {
|
||||
const dashboard = await savedObjectsClient.get('dashboard', id);
|
||||
const visualizations = [];
|
||||
export async function findRelationships(type, id, options = {}) {
|
||||
const {
|
||||
size,
|
||||
savedObjectsClient,
|
||||
savedObjectTypes,
|
||||
} = options;
|
||||
|
||||
// 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',
|
||||
}))
|
||||
);
|
||||
const { references = [] } = await savedObjectsClient.get(type, id);
|
||||
const bulkGetOpts = references.map(ref => ({ id: ref.id, type: ref.type }));
|
||||
|
||||
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([
|
||||
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,
|
||||
});
|
||||
}
|
||||
const relationshipObjects = [].concat(
|
||||
referencedObjects.saved_objects.map(extractCommonProperties),
|
||||
referencedResponse.saved_objects.map(extractCommonProperties),
|
||||
);
|
||||
|
||||
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 };
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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'),
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -62,6 +62,7 @@ export function registerScrollForExportRoute(server) {
|
|||
savedObjectVersion: 2
|
||||
},
|
||||
_migrationVersion: hit.migrationVersion,
|
||||
_references: hit.references || [],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
|
|
|
@ -74,6 +74,20 @@ function defaultMapping(): IndexMapping {
|
|||
updated_at: {
|
||||
type: 'date',
|
||||
},
|
||||
references: {
|
||||
type: 'nested',
|
||||
properties: {
|
||||
name: {
|
||||
type: 'keyword',
|
||||
},
|
||||
type: {
|
||||
type: 'keyword',
|
||||
},
|
||||
id: {
|
||||
type: 'keyword',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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: [] },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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: [],
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
),
|
||||
},
|
||||
|
|
|
@ -59,7 +59,8 @@ describe('POST /api/saved_objects/_bulk_get', () => {
|
|||
id: 'abc123',
|
||||
type: 'index-pattern',
|
||||
title: 'logstash-*',
|
||||
version: 2
|
||||
version: 2,
|
||||
references: [],
|
||||
}]
|
||||
};
|
||||
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
|
|
|
@ -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]);
|
||||
|
|
|
@ -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()
|
||||
},
|
||||
|
|
|
@ -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: [],
|
||||
}
|
||||
]
|
||||
};
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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-*',
|
||||
|
|
|
@ -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<Referencable>;
|
||||
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 }),
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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<string>} [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],
|
||||
};
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -99,13 +99,37 @@ function getClauseForType(schema, namespace, type) {
|
|||
* @param {String} search
|
||||
* @param {Array<string>} 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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -36,7 +36,7 @@ export interface BulkCreateObject<T extends SavedObjectAttributes = any> {
|
|||
}
|
||||
|
||||
export interface BulkCreateResponse<T extends SavedObjectAttributes = any> {
|
||||
savedObjects: Array<SavedObject<T>>;
|
||||
saved_objects: Array<SavedObject<T>>;
|
||||
}
|
||||
|
||||
export interface FindOptions extends BaseOptions {
|
||||
|
@ -68,7 +68,7 @@ export interface BulkGetObject {
|
|||
export type BulkGetObjects = BulkGetObject[];
|
||||
|
||||
export interface BulkGetResponse<T extends SavedObjectAttributes = any> {
|
||||
savedObjects: Array<SavedObject<T>>;
|
||||
saved_objects: Array<SavedObject<T>>;
|
||||
}
|
||||
|
||||
export interface SavedObjectAttributes {
|
||||
|
@ -84,6 +84,13 @@ export interface SavedObject<T extends SavedObjectAttributes = any> {
|
|||
message: string;
|
||||
};
|
||||
attributes: T;
|
||||
references: SavedObjectReference[];
|
||||
}
|
||||
|
||||
export interface SavedObjectReference {
|
||||
name: string;
|
||||
type: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export declare class SavedObjectsClient {
|
||||
|
|
|
@ -148,6 +148,7 @@ export class SavedObjectsClient {
|
|||
* @property {string} [options.sortOrder]
|
||||
* @property {Array<string>} [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 = {}) {
|
||||
|
|
|
@ -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 () {
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
};
|
||||
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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: [],
|
||||
},
|
||||
]
|
||||
});
|
||||
|
|
|
@ -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: [],
|
||||
}
|
||||
]
|
||||
});
|
||||
|
|
|
@ -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: [],
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -45,7 +45,8 @@ export default function ({ getService }) {
|
|||
version: 1,
|
||||
attributes: {
|
||||
'title': 'Count of requests'
|
||||
}
|
||||
},
|
||||
references: [],
|
||||
}
|
||||
]
|
||||
});
|
||||
|
|
|
@ -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',
|
||||
}],
|
||||
});
|
||||
})
|
||||
));
|
||||
|
|
|
@ -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: [] },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -51,7 +51,8 @@ export default function ({ getService }) {
|
|||
version: 2,
|
||||
attributes: {
|
||||
title: 'My second favorite vis'
|
||||
}
|
||||
},
|
||||
references: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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) {
|
||||
|
|
41
x-pack/plugins/graph/migrations.js
Normal file
41
x-pack/plugins/graph/migrations.js
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
||||
};
|
102
x-pack/plugins/graph/migrations.test.js
Normal file
102
x-pack/plugins/graph/migrations.test.js
Normal file
|
@ -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",
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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\\""`
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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}"
|
||||
}
|
||||
|
|
|
@ -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}"
|
||||
}
|
||||
|
|
|
@ -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}}"
|
||||
},
|
||||
|
|
|
@ -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\":[]}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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": "{}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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\":[]}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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": "{}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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": "{}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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": "{}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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++;
|
||||
}
|
||||
|
|
|
@ -149,6 +149,7 @@ export class SpacesSavedObjectsClient implements SavedObjectsClient {
|
|||
* @property {string} [options.sortOrder]
|
||||
* @property {Array<string>} [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 = {}) {
|
||||
|
|
|
@ -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: [],
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -69,6 +69,7 @@ export function bulkGetTestSuiteFactory(esArchiver: any, supertest: SuperTest<an
|
|||
attributes: {
|
||||
name: 'My favorite global object',
|
||||
},
|
||||
references: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
@ -102,6 +103,13 @@ export function bulkGetTestSuiteFactory(esArchiver: any, supertest: SuperTest<an
|
|||
uiStateJSON: resp.body.saved_objects[0].attributes.uiStateJSON,
|
||||
kibanaSavedObjectMeta: resp.body.saved_objects[0].attributes.kibanaSavedObjectMeta,
|
||||
},
|
||||
references: [
|
||||
{
|
||||
name: 'kibanaSavedObjectMeta.searchSourceJSON.index',
|
||||
type: 'index-pattern',
|
||||
id: `${getIdPrefix(spaceId)}91200a00-9efd-11e7-acb3-3dab96693fab`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: `${getIdPrefix(spaceId)}does not exist`,
|
||||
|
@ -119,6 +127,7 @@ export function bulkGetTestSuiteFactory(esArchiver: any, supertest: SuperTest<an
|
|||
attributes: {
|
||||
name: 'My favorite global object',
|
||||
},
|
||||
references: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
|
@ -67,6 +67,7 @@ export function createTestSuiteFactory(es: any, esArchiver: any, supertest: Supe
|
|||
attributes: {
|
||||
title: 'My favorite vis',
|
||||
},
|
||||
references: [],
|
||||
});
|
||||
|
||||
const expectedSpacePrefix = spaceId === DEFAULT_SPACE_ID ? '' : `${spaceId}:`;
|
||||
|
@ -107,6 +108,7 @@ export function createTestSuiteFactory(es: any, esArchiver: any, supertest: Supe
|
|||
attributes: {
|
||||
name: `Can't be contained to a space`,
|
||||
},
|
||||
references: [],
|
||||
});
|
||||
|
||||
// query ES directory to ensure namespace wasn't specified
|
||||
|
|
|
@ -67,6 +67,7 @@ export function findTestSuiteFactory(esArchiver: any, supertest: SuperTest<any>)
|
|||
attributes: {
|
||||
name: 'My favorite global object',
|
||||
},
|
||||
references: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
@ -99,6 +100,7 @@ export function findTestSuiteFactory(esArchiver: any, supertest: SuperTest<any>)
|
|||
attributes: {
|
||||
title: 'Count of requests',
|
||||
},
|
||||
references: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
|
@ -69,6 +69,7 @@ export function getTestSuiteFactory(esArchiver: any, supertest: SuperTest<any>)
|
|||
attributes: {
|
||||
name: 'My favorite global object',
|
||||
},
|
||||
references: [],
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -108,6 +109,13 @@ export function getTestSuiteFactory(esArchiver: any, supertest: SuperTest<any>)
|
|||
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`,
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -78,6 +78,7 @@ export function updateTestSuiteFactory(esArchiver: any, supertest: SuperTest<any
|
|||
attributes: {
|
||||
name: 'My second favorite',
|
||||
},
|
||||
references: [],
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -102,6 +103,7 @@ export function updateTestSuiteFactory(esArchiver: any, supertest: SuperTest<any
|
|||
attributes: {
|
||||
title: 'My second favorite vis',
|
||||
},
|
||||
references: [],
|
||||
});
|
||||
};
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue