[Graph] Deangularize saved object handling (#52529) (#53276)

This commit is contained in:
Matthias Wilhelm 2019-12-17 16:11:40 +01:00 committed by GitHub
parent d00c874d1b
commit 27f0782fca
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 164 additions and 163 deletions

View file

@ -1,62 +0,0 @@
/*
* 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 { SavedObjectProvider } from 'ui/saved_objects/saved_object';
import { i18n } from '@kbn/i18n';
import { extractReferences, injectReferences } from './saved_workspace_references';
export function SavedWorkspaceProvider(Private) {
// SavedWorkspace constructor. Usually you'd interact with an instance of this.
// ID is option, without it one will be generated on save.
const SavedObject = Private(SavedObjectProvider);
class SavedWorkspace extends SavedObject {
constructor(id) {
// Gives our SavedWorkspace the properties of a SavedObject
super({
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,
// default values that will get assigned if the doc is new
defaults: {
title: i18n.translate('xpack.graph.savedWorkspace.workspaceNameTitle', {
defaultMessage: 'New Graph Workspace',
}),
numLinks: 0,
numVertices: 0,
wsState: '{}',
version: 1,
},
});
// Overwrite the default getDisplayName function which uses type and which is not very
// user friendly for this object.
this.getDisplayName = function() {
return 'graph workspace';
};
}
} //End of class
SavedWorkspace.type = 'graph-workspace';
// if type:workspace has no mapping, we push this mapping into ES
SavedWorkspace.mapping = {
title: 'text',
description: 'text',
numLinks: 'integer',
numVertices: 'integer',
version: 'integer',
wsState: 'json',
};
SavedWorkspace.searchsource = false;
return SavedWorkspace;
}

View file

@ -0,0 +1,65 @@
/*
* 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 { SavedObject, SavedObjectKibanaServices } from 'ui/saved_objects/types';
import { createSavedObjectClass } from 'ui/saved_objects/saved_object';
import { i18n } from '@kbn/i18n';
import { extractReferences, injectReferences } from './saved_workspace_references';
export interface SavedWorkspace extends SavedObject {
wsState?: string;
}
export function createSavedWorkspaceClass(services: SavedObjectKibanaServices) {
// SavedWorkspace constructor. Usually you'd interact with an instance of this.
// ID is option, without it one will be generated on save.
const SavedObjectClass = createSavedObjectClass(services);
class SavedWorkspaceClass extends SavedObjectClass {
public static type: string = 'graph-workspace';
// if type:workspace has no mapping, we push this mapping into ES
public static mapping: Record<string, string> = {
title: 'text',
description: 'text',
numLinks: 'integer',
numVertices: 'integer',
version: 'integer',
wsState: 'json',
};
// Order these fields to the top, the rest are alphabetical
public static fieldOrder = ['title', 'description'];
public static searchSource = false;
public wsState?: string;
constructor(id: string) {
// Gives our SavedWorkspace the properties of a SavedObject
super({
type: SavedWorkspaceClass.type,
mapping: SavedWorkspaceClass.mapping,
searchSource: SavedWorkspaceClass.searchSource,
extractReferences,
injectReferences,
// if this is null/undefined then the SavedObject will be assigned the defaults
id,
// default values that will get assigned if the doc is new
defaults: {
title: i18n.translate('xpack.graph.savedWorkspace.workspaceNameTitle', {
defaultMessage: 'New Graph Workspace',
}),
numLinks: 0,
numVertices: 0,
wsState: '{}',
version: 1,
},
});
}
// Overwrite the default getDisplayName function which uses type and which is not very
// user friendly for this object.
getDisplayName = () => {
return 'graph workspace';
};
}
return SavedWorkspaceClass;
}

View file

@ -5,6 +5,7 @@
*/
import { extractReferences, injectReferences } from './saved_workspace_references';
import { SavedWorkspace } from './saved_workspace';
describe('extractReferences', () => {
test('extracts references from wsState', () => {
@ -19,6 +20,7 @@ describe('extractReferences', () => {
})
),
},
references: [],
};
const updatedDoc = extractReferences(doc);
expect(updatedDoc).toMatchInlineSnapshot(`
@ -48,6 +50,7 @@ Object {
})
),
},
references: [],
};
expect(() => extractReferences(doc)).toThrowErrorMatchingInlineSnapshot(
`"indexPattern attribute is missing in \\"wsState\\""`
@ -59,12 +62,12 @@ describe('injectReferences', () => {
test('injects references into context', () => {
const context = {
id: '1',
foo: true,
title: 'test',
wsState: JSON.stringify({
indexPatternRefName: 'indexPattern_0',
bar: true,
}),
};
} as SavedWorkspace;
const references = [
{
name: 'indexPattern_0',
@ -75,8 +78,8 @@ describe('injectReferences', () => {
injectReferences(context, references);
expect(context).toMatchInlineSnapshot(`
Object {
"foo": true,
"id": "1",
"title": "test",
"wsState": "{\\"bar\\":true,\\"indexPattern\\":\\"pattern*\\"}",
}
`);
@ -85,13 +88,13 @@ Object {
test('skips when wsState is not a string', () => {
const context = {
id: '1',
foo: true,
};
title: 'test',
} as SavedWorkspace;
injectReferences(context, []);
expect(context).toMatchInlineSnapshot(`
Object {
"foo": true,
"id": "1",
"title": "test",
}
`);
});
@ -100,7 +103,7 @@ Object {
const context = {
id: '1',
wsState: JSON.stringify({ bar: true }),
};
} as SavedWorkspace;
injectReferences(context, []);
expect(context).toMatchInlineSnapshot(`
Object {
@ -116,7 +119,7 @@ Object {
wsState: JSON.stringify({
indexPatternRefName: 'indexPattern_0',
}),
};
} as SavedWorkspace;
expect(() => injectReferences(context, [])).toThrowErrorMatchingInlineSnapshot(
`"Could not find reference \\"indexPattern_0\\""`
);

View file

@ -4,9 +4,18 @@
* you may not use this file except in compliance with the Elastic License.
*/
export function extractReferences({ attributes, references = [] }) {
import { SavedObjectAttributes, SavedObjectReference } from 'kibana/public';
import { SavedWorkspace } from './saved_workspace';
export function extractReferences({
attributes,
references = [],
}: {
attributes: SavedObjectAttributes;
references: SavedObjectReference[];
}) {
// For some reason, wsState comes in stringified 2x
const state = JSON.parse(JSON.parse(attributes.wsState));
const state = JSON.parse(JSON.parse(String(attributes.wsState)));
const { indexPattern } = state;
if (!indexPattern) {
throw new Error('indexPattern attribute is missing in "wsState"');
@ -29,7 +38,7 @@ export function extractReferences({ attributes, references = [] }) {
};
}
export function injectReferences(savedObject, references) {
export function injectReferences(savedObject: SavedWorkspace, references: SavedObjectReference[]) {
// 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') {

View file

@ -1,90 +0,0 @@
/*
* 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 _ from 'lodash';
import chrome from 'ui/chrome';
import { SavedObjectRegistryProvider } from 'ui/saved_objects/saved_object_registry';
import { SavedObjectsClientProvider } from 'ui/saved_objects';
import { i18n } from '@kbn/i18n';
import { SavedWorkspaceProvider } from './saved_workspace';
export function SavedWorkspacesProvider(kbnUrl, Private, Promise) {
const savedObjectsClient = Private(SavedObjectsClientProvider);
const SavedWorkspace = Private(SavedWorkspaceProvider);
this.type = SavedWorkspace.type;
this.Class = SavedWorkspace;
this.loaderProperties = {
name: 'Graph workspace',
noun: i18n.translate('xpack.graph.savedWorkspaces.graphWorkspaceLabel', {
defaultMessage: 'Graph workspace',
}),
nouns: i18n.translate('xpack.graph.savedWorkspaces.graphWorkspacesLabel', {
defaultMessage: 'Graph workspaces',
}),
};
// Returns a single dashboard by ID, should be the name of the workspace
this.get = function(id) {
// Returns a promise that contains a workspace which is a subclass of docSource
return new SavedWorkspace(id).init();
};
this.urlFor = function(id) {
return chrome.addBasePath(kbnUrl.eval('/app/graph#/workspace/{{id}}', { id }));
};
this.delete = function(ids) {
ids = !_.isArray(ids) ? [ids] : ids;
return Promise.map(ids, function(id) {
return new SavedWorkspace(id).delete();
});
};
this.mapHits = function(hit) {
const source = hit.attributes;
source.id = hit.id;
source.url = this.urlFor(hit.id);
source.icon = 'fa-share-alt'; // looks like a graph
return source;
};
this.find = function(searchString, size = 100) {
let body;
if (searchString) {
body = {
query: {
simple_query_string: {
query: searchString + '*',
fields: ['title^3', 'description'],
default_operator: 'AND',
},
},
};
} else {
body = { query: { match_all: {} } };
}
return savedObjectsClient
.find({
type: SavedWorkspace.type,
search: searchString ? `${searchString}*` : undefined,
perPage: size,
searchFields: ['title^3', 'description'],
})
.then(resp => {
return {
total: resp.total,
hits: resp.savedObjects.map(hit => this.mapHits(hit)),
};
});
};
}
SavedObjectRegistryProvider.register(SavedWorkspacesProvider);

View file

@ -0,0 +1,76 @@
/*
* 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 { npSetup, npStart } from 'ui/new_platform';
import { SavedObjectRegistryProvider } from 'ui/saved_objects/saved_object_registry';
import { i18n } from '@kbn/i18n';
import { createSavedWorkspaceClass } from './saved_workspace';
export function SavedWorkspacesProvider() {
const savedObjectsClient = npStart.core.savedObjects.client;
const services = {
savedObjectsClient,
indexPatterns: npStart.plugins.data.indexPatterns,
chrome: npStart.core.chrome,
overlays: npStart.core.overlays,
};
const SavedWorkspace = createSavedWorkspaceClass(services);
const urlFor = (id: string) =>
npSetup.core.http.basePath.prepend(`/app/graph#/workspace/${encodeURIComponent(id)}`);
const mapHits = (hit: { id: string; attributes: Record<string, unknown> }) => {
const source = hit.attributes;
source.id = hit.id;
source.url = urlFor(hit.id);
source.icon = 'fa-share-alt'; // looks like a graph
return source;
};
return {
type: SavedWorkspace.type,
Class: SavedWorkspace,
loaderProperties: {
name: 'Graph workspace',
noun: i18n.translate('xpack.graph.savedWorkspaces.graphWorkspaceLabel', {
defaultMessage: 'Graph workspace',
}),
nouns: i18n.translate('xpack.graph.savedWorkspaces.graphWorkspacesLabel', {
defaultMessage: 'Graph workspaces',
}),
},
// Returns a single dashboard by ID, should be the name of the workspace
get: (id: string) => {
// Returns a promise that contains a workspace which is a subclass of docSource
// @ts-ignore
return new SavedWorkspace(id).init();
},
urlFor,
delete: (ids: string | string[]) => {
const idArr = Array.isArray(ids) ? ids : [ids];
return Promise.all(
idArr.map((id: string) => savedObjectsClient.delete(SavedWorkspace.type, id))
);
},
find: (searchString: string, size: number = 100) => {
return savedObjectsClient
.find({
type: SavedWorkspace.type,
search: searchString ? `${searchString}*` : undefined,
perPage: size,
searchFields: ['title^3', 'description'],
})
.then(resp => {
return {
total: resp.total,
hits: resp.savedObjects.map(hit => mapHits(hit)),
};
});
},
};
}
SavedObjectRegistryProvider.register(SavedWorkspacesProvider);