Skip to content

Commit

Permalink
[Graph] Fix graph saved object references (#85295) (#85520)
Browse files Browse the repository at this point in the history
  • Loading branch information
flash1293 authored Dec 10, 2020
1 parent f8f6c3f commit 207fa22
Show file tree
Hide file tree
Showing 15 changed files with 238 additions and 35 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);
});
});
});
38 changes: 29 additions & 9 deletions x-pack/plugins/graph/public/services/persistence/deserialize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,19 +61,39 @@ function deserializeUrlTemplate({
return template;
}

// returns the id of the index pattern, lookup is done in app.js
export function lookupIndexPattern(
/**
* Migrates `savedWorkspace` to use the id instead of the title of the referenced index pattern.
* Returns a status indicating successful migration or failure to look up the index pattern by title.
* If the workspace is migrated already, a success status is returned as well.
* @param savedWorkspace The workspace saved object to migrate. The migration will happen in-place and mutate the passed in object
* @param indexPatterns All index patterns existing in the current space
*/
export function migrateLegacyIndexPatternRef(
savedWorkspace: GraphWorkspaceSavedObject,
indexPatterns: IndexPatternSavedObject[]
) {
): { success: true } | { success: false; missingIndexPattern: string } {
const legacyIndexPatternRef = savedWorkspace.legacyIndexPatternRef;
if (!legacyIndexPatternRef) {
return { success: true };
}
const indexPatternId = indexPatterns.find(
(pattern) => pattern.attributes.title === legacyIndexPatternRef
)?.id;
if (!indexPatternId) {
return { success: false, missingIndexPattern: legacyIndexPatternRef };
}
const serializedWorkspaceState: SerializedWorkspaceState = JSON.parse(savedWorkspace.wsState);
const indexPattern = indexPatterns.find(
(pattern) => pattern.attributes.title === serializedWorkspaceState.indexPattern
);
serializedWorkspaceState.indexPattern = indexPatternId!;
savedWorkspace.wsState = JSON.stringify(serializedWorkspaceState);
delete savedWorkspace.legacyIndexPatternRef;
return { success: true };
}

if (indexPattern) {
return indexPattern;
}
// 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,
},
},
},
};
Loading

0 comments on commit 207fa22

Please sign in to comment.