Skip to content

Commit

Permalink
straighten graph saved object references
Browse files Browse the repository at this point in the history
  • Loading branch information
flash1293 committed Dec 8, 2020
1 parent bdd0e25 commit 55667df
Show file tree
Hide file tree
Showing 15 changed files with 231 additions and 34 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { GraphWorkspaceSavedObject, Workspace } from '../../types';
import { savedWorkspaceToAppState } from './deserialize';
import { GraphWorkspaceSavedObject, IndexPatternSavedObject, Workspace } from '../../types';
import { migrateLegacyIndexPatternRef, savedWorkspaceToAppState } from './deserialize';
import { createWorkspace } from '../../angular/graph_client_workspace';
import { outlinkEncoders } from '../../helpers/outlink_encoders';
import { IndexPattern } from '../../../../../../src/plugins/data/public';
Expand All @@ -21,7 +21,7 @@ describe('deserialize', () => {
numLinks: 2,
numVertices: 4,
wsState: JSON.stringify({
indexPattern: 'Testindexpattern',
indexPattern: '123',
selectedFields: [
{ color: 'black', name: 'field1', selected: true, iconClass: 'a' },
{ color: 'black', name: 'field2', selected: true, iconClass: 'b' },
Expand Down Expand Up @@ -208,4 +208,32 @@ describe('deserialize', () => {
expect(workspace.edges[1].source).toBe(workspace.nodes[2]);
expect(workspace.edges[1].target).toBe(workspace.nodes[4]);
});

describe('migrateLegacyIndexPatternRef', () => {
it('should migrate legacy index pattern ref', () => {
const workspacePayload = { ...savedWorkspace, legacyIndexPatternRef: 'Testpattern' };
const success = migrateLegacyIndexPatternRef(workspacePayload, [
{ id: '678', attributes: { title: 'Testpattern' } } as IndexPatternSavedObject,
{ id: '123', attributes: { title: 'otherpattern' } } as IndexPatternSavedObject,
]);
expect(success).toEqual({ success: true });
expect(workspacePayload.legacyIndexPatternRef).toBeUndefined();
expect(JSON.parse(workspacePayload.wsState).indexPattern).toBe('678');
});

it('should return false if migration fails', () => {
const workspacePayload = { ...savedWorkspace, legacyIndexPatternRef: 'Testpattern' };
const success = migrateLegacyIndexPatternRef(workspacePayload, [
{ id: '123', attributes: { title: 'otherpattern' } } as IndexPatternSavedObject,
]);
expect(success).toEqual({ success: false, missingIndexPattern: 'Testpattern' });
});

it('should not modify migrated workspaces', () => {
const workspacePayload = { ...savedWorkspace };
const success = migrateLegacyIndexPatternRef(workspacePayload, []);
expect(success).toEqual({ success: true });
expect(workspacePayload).toEqual(savedWorkspace);
});
});
});
30 changes: 22 additions & 8 deletions x-pack/plugins/graph/public/services/persistence/deserialize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,18 +62,32 @@ function deserializeUrlTemplate({
}

// returns the id of the index pattern, lookup is done in app.js
export function lookupIndexPattern(
export function migrateLegacyIndexPatternRef(
savedWorkspace: GraphWorkspaceSavedObject,
indexPatterns: IndexPatternSavedObject[]
) {
): { success: true } | { success: false; missingIndexPattern: string } {
const legacyIndexPatternRef = savedWorkspace.legacyIndexPatternRef;
if (!legacyIndexPatternRef) {
return { success: true };
}
const serializedWorkspaceState: SerializedWorkspaceState = JSON.parse(savedWorkspace.wsState);
const indexPattern = indexPatterns.find(
(pattern) => pattern.attributes.title === serializedWorkspaceState.indexPattern
);

if (indexPattern) {
return indexPattern;
const indexPatternId = indexPatterns.find(
(pattern) => pattern.attributes.title === legacyIndexPatternRef
)?.id;
if (!indexPatternId) {
return { success: false, missingIndexPattern: legacyIndexPatternRef };
}
serializedWorkspaceState.indexPattern = indexPatternId!;
savedWorkspace.wsState = JSON.stringify(serializedWorkspaceState);
delete savedWorkspace.legacyIndexPatternRef;
return { success: true };
}

// returns the id of the index pattern, lookup is done in app.js
export function lookupIndexPatternId(savedWorkspace: GraphWorkspaceSavedObject) {
const serializedWorkspaceState: SerializedWorkspaceState = JSON.parse(savedWorkspace.wsState);

return serializedWorkspaceState.indexPattern;
}

// returns all graph fields mapped out of the index pattern
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ describe('serialize', () => {
"timeoutMillis": 5000,
"useSignificance": true,
},
"indexPattern": "Testindexpattern",
"indexPattern": "123",
"links": Array [
Object {
"label": "",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ export function appStateToSavedWorkspace(
const mappedUrlTemplates = urlTemplates.map(serializeUrlTemplate);

const persistedWorkspaceState: SerializedWorkspaceState = {
indexPattern: selectedIndex.title,
indexPattern: selectedIndex.id,
selectedFields: selectedFields.map(serializeField),
blocklist,
vertices,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,10 @@ export const datasourceSaga = ({
yield put(setDatasource({ type: 'none' }));
notifications.toasts.addDanger(
i18n.translate('xpack.graph.loadWorkspace.missingIndexPatternErrorMessage', {
defaultMessage: 'Index pattern not found',
defaultMessage: 'Index pattern "{name}" not found',
values: {
name: action.payload.title,
},
})
);
}
Expand Down
4 changes: 3 additions & 1 deletion x-pack/plugins/graph/public/state_management/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,9 @@ export function createMockGraphStore({
getWorkspace: jest.fn(() => workspaceMock),
getSavedWorkspace: jest.fn(() => savedWorkspace),
indexPatternProvider: {
get: jest.fn(() => Promise.resolve(({} as unknown) as IndexPattern)),
get: jest.fn(() =>
Promise.resolve(({ id: '123', title: 'test-pattern' } as unknown) as IndexPattern)
),
},
indexPatterns: [
({ id: '123', attributes: { title: 'test-pattern' } } as unknown) as IndexPatternSavedObject,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,15 @@ import { IndexpatternDatasource, datasourceSelector } from './datasource';
import { fieldsSelector } from './fields';
import { metaDataSelector, updateMetaData } from './meta_data';
import { templatesSelector } from './url_templates';
import { lookupIndexPattern, appStateToSavedWorkspace } from '../services/persistence';
import { migrateLegacyIndexPatternRef, appStateToSavedWorkspace } from '../services/persistence';
import { settingsSelector } from './advanced_settings';
import { openSaveModal } from '../services/save_modal';

const waitForPromise = () => new Promise((r) => setTimeout(r));

jest.mock('../services/persistence', () => ({
lookupIndexPattern: jest.fn(() => ({ id: '123', attributes: { title: 'test-pattern' } })),
lookupIndexPatternId: jest.fn(() => ({ id: '123', attributes: { title: 'test-pattern' } })),
migrateLegacyIndexPatternRef: jest.fn(() => ({ success: true })),
savedWorkspaceToAppState: jest.fn(() => ({
urlTemplates: [
{
Expand Down Expand Up @@ -67,7 +68,7 @@ describe('persistence sagas', () => {
});

it('should warn with a toast and abort if index pattern is not found', async () => {
(lookupIndexPattern as jest.Mock).mockReturnValueOnce(undefined);
(migrateLegacyIndexPatternRef as jest.Mock).mockReturnValueOnce({ success: false });
env.store.dispatch(loadSavedWorkspace({} as GraphWorkspaceSavedObject));
await waitForPromise();
expect(env.mockedDeps.notifications.toasts.addDanger).toHaveBeenCalled();
Expand Down
30 changes: 18 additions & 12 deletions x-pack/plugins/graph/public/state_management/persistence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ import { loadFields, selectedFieldsSelector } from './fields';
import { updateSettings, settingsSelector } from './advanced_settings';
import { loadTemplates, templatesSelector } from './url_templates';
import {
lookupIndexPattern,
migrateLegacyIndexPatternRef,
savedWorkspaceToAppState,
appStateToSavedWorkspace,
lookupIndexPatternId,
} from '../services/persistence';
import { updateMetaData, metaDataSelector } from './meta_data';
import { openSaveModal, SaveWorkspaceHandler } from '../services/save_modal';
Expand All @@ -43,23 +44,28 @@ export const loadingSaga = ({
indexPatternProvider,
}: GraphStoreDependencies) => {
function* deserializeWorkspace(action: Action<GraphWorkspaceSavedObject>) {
const selectedIndex = lookupIndexPattern(action.payload, indexPatterns);
if (!selectedIndex) {
const workspacePayload = action.payload;
const migrationStatus = migrateLegacyIndexPatternRef(workspacePayload, indexPatterns);
if (!migrationStatus.success) {
notifications.toasts.addDanger(
i18n.translate('xpack.graph.loadWorkspace.missingIndexPatternErrorMessage', {
defaultMessage: 'Index pattern not found',
defaultMessage: 'Index pattern "{name}" not found',
values: {
name: migrationStatus.missingIndexPattern,
},
})
);
return;
}

const indexPattern = yield call(indexPatternProvider.get, selectedIndex.id);
const selectedIndexPatternId = lookupIndexPatternId(workspacePayload);
const indexPattern = yield call(indexPatternProvider.get, selectedIndexPatternId);
const initialSettings = settingsSelector(yield select());

createWorkspace(selectedIndex.attributes.title, initialSettings);
createWorkspace(indexPattern.title, initialSettings);

const { urlTemplates, advancedSettings, allFields } = savedWorkspaceToAppState(
action.payload,
workspacePayload,
indexPattern,
// workspace won't be null because it's created in the same call stack
getWorkspace()!
Expand All @@ -68,16 +74,16 @@ export const loadingSaga = ({
// put everything in the store
yield put(
updateMetaData({
title: action.payload.title,
description: action.payload.description,
savedObjectId: action.payload.id,
title: workspacePayload.title,
description: workspacePayload.description,
savedObjectId: workspacePayload.id,
})
);
yield put(
setDatasource({
type: 'indexpattern',
id: selectedIndex.id,
title: selectedIndex.attributes.title,
id: indexPattern.id,
title: indexPattern.title,
})
);
yield put(loadFields(allFields));
Expand Down
4 changes: 4 additions & 0 deletions x-pack/plugins/graph/public/types/persistence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,14 @@ export interface GraphWorkspaceSavedObject {
type: string;
version?: number;
wsState: string;
// the title of the index pattern used by this workspace.
// Only set for legacy saved objects.
legacyIndexPatternRef?: string;
_source: Record<string, unknown>;
}

export interface SerializedWorkspaceState {
// the id of the index pattern saved object
indexPattern: string;
selectedFields: SerializedField[];
blocklist: SerializedNode[];
Expand Down
4 changes: 2 additions & 2 deletions x-pack/plugins/graph/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export class GraphPlugin implements Plugin {
all: ['graph-workspace'],
read: ['index-pattern'],
},
ui: ['save', 'delete'],
ui: ['save', 'delete', 'show'],
},
read: {
app: ['graph', 'kibana'],
Expand All @@ -69,7 +69,7 @@ export class GraphPlugin implements Plugin {
all: [],
read: ['index-pattern', 'graph-workspace'],
},
ui: [],
ui: ['show'],
},
},
});
Expand Down
18 changes: 18 additions & 0 deletions x-pack/plugins/graph/server/saved_objects/graph_workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,20 @@ export const graphWorkspace: SavedObjectsType = {
name: 'graph-workspace',
namespaceType: 'single',
hidden: false,
management: {
icon: 'graphApp',
defaultSearchField: 'title',
importableAndExportable: true,
getTitle(obj) {
return obj.attributes.title;
},
getInAppUrl(obj) {
return {
path: `/app/graph#/workspace/${encodeURIComponent(obj.id)}`,
uiCapabilitiesPath: 'graph.show',
};
},
},
migrations: graphMigrations,
mappings: {
properties: {
Expand Down Expand Up @@ -38,6 +52,10 @@ export const graphWorkspace: SavedObjectsType = {
wsState: {
type: 'text',
},
legacyIndexPatternRef: {
type: 'text',
index: false,
},
},
},
};
92 changes: 92 additions & 0 deletions x-pack/plugins/graph/server/saved_objects/migrations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,4 +108,96 @@ describe('graph-workspace', () => {
`);
});
});

describe('7.11', () => {
const migration = graphMigrations['7.11.0'];

test('remove broken reference and set legacy attribute', () => {
const doc = {
id: '1',
type: 'graph-workspace',
attributes: {
wsState: JSON.stringify(
JSON.stringify({ foo: true, indexPatternRefName: 'indexPattern_0' })
),
bar: true,
},
references: [
{
id: 'pattern*',
name: 'indexPattern_0',
type: 'index-pattern',
},
],
};
const migratedDoc = migration(doc);
expect(migratedDoc).toMatchInlineSnapshot(`
Object {
"attributes": Object {
"bar": true,
"legacyIndexPatternRef": "pattern*",
"wsState": "\\"{\\\\\\"foo\\\\\\":true}\\"",
},
"id": "1",
"references": Array [],
"type": "graph-workspace",
}
`);
});

test('bails out on missing reference', () => {
const doc = {
id: '1',
type: 'graph-workspace',
attributes: {
wsState: JSON.stringify(
JSON.stringify({ foo: true, indexPatternRefName: 'indexPattern_0' })
),
bar: true,
},
};
const migratedDoc = migration(doc);
expect(migratedDoc).toBe(doc);
});

test('bails out on missing index pattern in state', () => {
const doc = {
id: '1',
type: 'graph-workspace',
attributes: {
wsState: JSON.stringify(JSON.stringify({ foo: true })),
bar: true,
},
references: [
{
id: 'pattern*',
name: 'indexPattern_0',
type: 'index-pattern',
},
],
};
const migratedDoc = migration(doc);
expect(migratedDoc).toBe(doc);
});

test('bails out on broken wsState', () => {
const doc = {
id: '1',
type: 'graph-workspace',
attributes: {
wsState: '{{[[',
bar: true,
},
references: [
{
id: 'pattern*',
name: 'indexPattern_0',
type: 'index-pattern',
},
],
};
const migratedDoc = migration(doc);
expect(migratedDoc).toBe(doc);
});
});
});
Loading

0 comments on commit 55667df

Please sign in to comment.