diff --git a/x-pack/plugins/lens/common/types.ts b/x-pack/plugins/lens/common/types.ts
index 56a56bdc2d59c..c572b59899fce 100644
--- a/x-pack/plugins/lens/common/types.ts
+++ b/x-pack/plugins/lens/common/types.ts
@@ -4,6 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { FilterMeta, Filter } from 'src/plugins/data/common';
+
export interface ExistingFields {
indexPatternTitle: string;
existingFieldNames: string[];
@@ -13,3 +15,11 @@ export interface DateRange {
fromDate: string;
toDate: string;
}
+
+export interface PersistableFilterMeta extends FilterMeta {
+ indexRefName?: string;
+}
+
+export interface PersistableFilter extends Filter {
+ meta: PersistableFilterMeta;
+}
diff --git a/x-pack/plugins/lens/public/app_plugin/app.test.tsx b/x-pack/plugins/lens/public/app_plugin/app.test.tsx
index f92343183a700..b1d1fbd40f485 100644
--- a/x-pack/plugins/lens/public/app_plugin/app.test.tsx
+++ b/x-pack/plugins/lens/public/app_plugin/app.test.tsx
@@ -33,7 +33,7 @@ import { navigationPluginMock } from '../../../../../src/plugins/navigation/publ
import { TopNavMenuData } from '../../../../../src/plugins/navigation/public';
import { coreMock } from 'src/core/public/mocks';
-jest.mock('../persistence');
+jest.mock('../editor_frame_service/editor_frame/expression_helpers');
jest.mock('src/core/public');
jest.mock('../../../../../src/plugins/saved_objects/public', () => {
// eslint-disable-next-line no-shadow
@@ -282,11 +282,11 @@ describe('Lens App', () => {
(defaultArgs.docStorage.load as jest.Mock).mockResolvedValue({
id: '1234',
title: 'Daaaaaaadaumching!',
- expression: 'valid expression',
state: {
query: 'fake query',
- datasourceMetaData: { filterableIndexPatterns: [{ id: '1', title: 'saved' }] },
+ filters: [],
},
+ references: [],
});
await act(async () => {
instance.setProps({ docId: '1234' });
@@ -312,12 +312,11 @@ describe('Lens App', () => {
args.editorFrame = frame;
(args.docStorage.load as jest.Mock).mockResolvedValue({
id: '1234',
- expression: 'valid expression',
state: {
query: 'fake query',
filters: [{ query: { match_phrase: { src: 'test' } } }],
- datasourceMetaData: { filterableIndexPatterns: [{ id: '1', title: 'saved' }] },
},
+ references: [{ type: 'index-pattern', id: '1', name: 'index-pattern-0' }],
});
instance = mount();
@@ -341,15 +340,13 @@ describe('Lens App', () => {
expect(frame.mount).toHaveBeenCalledWith(
expect.any(Element),
expect.objectContaining({
- doc: {
+ doc: expect.objectContaining({
id: '1234',
- expression: 'valid expression',
- state: {
+ state: expect.objectContaining({
query: 'fake query',
filters: [{ query: { match_phrase: { src: 'test' } } }],
- datasourceMetaData: { filterableIndexPatterns: [{ id: '1', title: 'saved' }] },
- },
- },
+ }),
+ }),
})
);
});
@@ -410,7 +407,6 @@ describe('Lens App', () => {
expression: 'valid expression',
state: {
query: 'kuery',
- datasourceMetaData: { filterableIndexPatterns: [{ id: '1', title: 'saved' }] },
},
} as jest.ResolvedValue);
});
@@ -433,7 +429,12 @@ describe('Lens App', () => {
}
async function save({
- lastKnownDoc = { expression: 'kibana 3' },
+ lastKnownDoc = {
+ references: [],
+ state: {
+ filters: [],
+ },
+ },
initialDocId,
...saveProps
}: SaveProps & {
@@ -447,16 +448,14 @@ describe('Lens App', () => {
args.editorFrame = frame;
(args.docStorage.load as jest.Mock).mockResolvedValue({
id: '1234',
- expression: 'kibana',
+ references: [],
state: {
query: 'fake query',
- datasourceMetaData: { filterableIndexPatterns: [{ id: '1', title: 'saved' }] },
filters: [],
},
});
(args.docStorage.save as jest.Mock).mockImplementation(async ({ id }) => ({
id: id || 'aaa',
- expression: 'kibana 2',
}));
await act(async () => {
@@ -474,6 +473,7 @@ describe('Lens App', () => {
onChange({
filterableIndexPatterns: [],
doc: { id: initialDocId, ...lastKnownDoc } as Document,
+ isSaveable: true,
})
);
@@ -507,7 +507,8 @@ describe('Lens App', () => {
act(() =>
onChange({
filterableIndexPatterns: [],
- doc: ({ id: 'will save this', expression: 'valid expression' } as unknown) as Document,
+ doc: ({ id: 'will save this' } as unknown) as Document,
+ isSaveable: true,
})
);
instance.update();
@@ -526,7 +527,8 @@ describe('Lens App', () => {
act(() =>
onChange({
filterableIndexPatterns: [],
- doc: ({ id: 'will save this', expression: 'valid expression' } as unknown) as Document,
+ doc: ({ id: 'will save this' } as unknown) as Document,
+ isSaveable: true,
})
);
instance.update();
@@ -541,11 +543,12 @@ describe('Lens App', () => {
newTitle: 'hello there',
});
- expect(args.docStorage.save).toHaveBeenCalledWith({
- id: undefined,
- title: 'hello there',
- expression: 'kibana 3',
- });
+ expect(args.docStorage.save).toHaveBeenCalledWith(
+ expect.objectContaining({
+ id: undefined,
+ title: 'hello there',
+ })
+ );
expect(args.redirectTo).toHaveBeenCalledWith('aaa', undefined, true);
@@ -561,11 +564,12 @@ describe('Lens App', () => {
newTitle: 'hello there',
});
- expect(args.docStorage.save).toHaveBeenCalledWith({
- id: undefined,
- title: 'hello there',
- expression: 'kibana 3',
- });
+ expect(args.docStorage.save).toHaveBeenCalledWith(
+ expect.objectContaining({
+ id: undefined,
+ title: 'hello there',
+ })
+ );
expect(args.redirectTo).toHaveBeenCalledWith('aaa', undefined, true);
@@ -581,11 +585,12 @@ describe('Lens App', () => {
newTitle: 'hello there',
});
- expect(args.docStorage.save).toHaveBeenCalledWith({
- id: '1234',
- title: 'hello there',
- expression: 'kibana 3',
- });
+ expect(args.docStorage.save).toHaveBeenCalledWith(
+ expect.objectContaining({
+ id: '1234',
+ title: 'hello there',
+ })
+ );
expect(args.redirectTo).not.toHaveBeenCalled();
@@ -605,7 +610,8 @@ describe('Lens App', () => {
act(() =>
onChange({
filterableIndexPatterns: [],
- doc: ({ id: undefined, expression: 'new expression' } as unknown) as Document,
+ doc: ({ id: undefined } as unknown) as Document,
+ isSaveable: true,
})
);
@@ -629,11 +635,12 @@ describe('Lens App', () => {
newTitle: 'hello there',
});
- expect(args.docStorage.save).toHaveBeenCalledWith({
- expression: 'kibana 3',
- id: undefined,
- title: 'hello there',
- });
+ expect(args.docStorage.save).toHaveBeenCalledWith(
+ expect.objectContaining({
+ id: undefined,
+ title: 'hello there',
+ })
+ );
expect(args.redirectTo).toHaveBeenCalledWith('aaa', true, true);
});
@@ -683,7 +690,8 @@ describe('Lens App', () => {
await act(async () =>
onChange({
filterableIndexPatterns: [],
- doc: ({ id: '123', expression: 'valid expression' } as unknown) as Document,
+ doc: ({ id: '123' } as unknown) as Document,
+ isSaveable: true,
})
);
instance.update();
@@ -722,7 +730,8 @@ describe('Lens App', () => {
await act(async () =>
onChange({
filterableIndexPatterns: [],
- doc: ({ expression: 'valid expression' } as unknown) as Document,
+ doc: ({} as unknown) as Document,
+ isSaveable: true,
})
);
instance.update();
@@ -745,7 +754,6 @@ describe('Lens App', () => {
expression: 'valid expression',
state: {
query: 'kuery',
- datasourceMetaData: { filterableIndexPatterns: [{ id: '1', title: 'saved' }] },
},
} as jest.ResolvedValue);
});
@@ -790,8 +798,9 @@ describe('Lens App', () => {
await act(async () => {
onChange({
- filterableIndexPatterns: [{ id: '1', title: 'newIndex' }],
- doc: ({ id: undefined, expression: 'valid expression' } as unknown) as Document,
+ filterableIndexPatterns: ['1'],
+ doc: ({ id: undefined } as unknown) as Document,
+ isSaveable: true,
});
});
@@ -808,8 +817,9 @@ describe('Lens App', () => {
await act(async () => {
onChange({
- filterableIndexPatterns: [{ id: '2', title: 'second index' }],
- doc: ({ id: undefined, expression: 'valid expression' } as unknown) as Document,
+ filterableIndexPatterns: ['2'],
+ doc: ({ id: undefined } as unknown) as Document,
+ isSaveable: true,
});
});
@@ -1044,11 +1054,11 @@ describe('Lens App', () => {
(defaultArgs.docStorage.load as jest.Mock).mockResolvedValue({
id: '1234',
title: 'My cool doc',
- expression: 'valid expression',
state: {
query: 'kuery',
- datasourceMetaData: { filterableIndexPatterns: [{ id: '1', title: 'saved' }] },
+ filters: [],
},
+ references: [],
} as jest.ResolvedValue);
});
@@ -1080,7 +1090,12 @@ describe('Lens App', () => {
act(() =>
onChange({
filterableIndexPatterns: [],
- doc: ({ id: undefined, expression: 'valid expression' } as unknown) as Document,
+ doc: ({
+ id: undefined,
+
+ references: [],
+ } as unknown) as Document,
+ isSaveable: true,
})
);
instance.update();
@@ -1101,7 +1116,8 @@ describe('Lens App', () => {
act(() =>
onChange({
filterableIndexPatterns: [],
- doc: ({ id: undefined, expression: 'valid expression' } as unknown) as Document,
+ doc: ({ id: undefined, state: {} } as unknown) as Document,
+ isSaveable: true,
})
);
instance.update();
@@ -1125,7 +1141,12 @@ describe('Lens App', () => {
act(() =>
onChange({
filterableIndexPatterns: [],
- doc: ({ id: '1234', expression: 'different expression' } as unknown) as Document,
+ doc: ({
+ id: '1234',
+
+ references: [],
+ } as unknown) as Document,
+ isSaveable: true,
})
);
instance.update();
@@ -1149,7 +1170,16 @@ describe('Lens App', () => {
act(() =>
onChange({
filterableIndexPatterns: [],
- doc: ({ id: '1234', expression: 'valid expression' } as unknown) as Document,
+ doc: ({
+ id: '1234',
+ title: 'My cool doc',
+ references: [],
+ state: {
+ query: 'kuery',
+ filters: [],
+ },
+ } as unknown) as Document,
+ isSaveable: true,
})
);
instance.update();
@@ -1173,7 +1203,8 @@ describe('Lens App', () => {
act(() =>
onChange({
filterableIndexPatterns: [],
- doc: ({ id: '1234', expression: null } as unknown) as Document,
+ doc: ({ id: '1234', references: [] } as unknown) as Document,
+ isSaveable: true,
})
);
instance.update();
diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx
index b20fe2f804683..31743e5b56b3a 100644
--- a/x-pack/plugins/lens/public/app_plugin/app.tsx
+++ b/x-pack/plugins/lens/public/app_plugin/app.tsx
@@ -27,7 +27,7 @@ import {
OnSaveProps,
checkForDuplicateTitle,
} from '../../../../../src/plugins/saved_objects/public';
-import { Document, SavedObjectStore } from '../persistence';
+import { Document, SavedObjectStore, injectFilterReferences } from '../persistence';
import { EditorFrameInstance } from '../types';
import { NativeRenderer } from '../native_renderer';
import { trackUiEvent } from '../lens_ui_telemetry';
@@ -56,6 +56,7 @@ interface State {
query: Query;
filters: Filter[];
savedQuery?: SavedQuery;
+ isSaveable: boolean;
}
export function App({
@@ -99,6 +100,7 @@ export function App({
originatingApp,
filters: data.query.filterManager.getFilters(),
indicateNoData: false,
+ isSaveable: false,
};
});
@@ -121,11 +123,7 @@ export function App({
const { lastKnownDoc } = state;
- const isSaveable =
- lastKnownDoc &&
- lastKnownDoc.expression &&
- lastKnownDoc.expression.length > 0 &&
- core.application.capabilities.visualize.save;
+ const savingPermitted = state.isSaveable && core.application.capabilities.visualize.save;
useEffect(() => {
// Clear app-specific filters when navigating to Lens. Necessary because Lens
@@ -176,15 +174,34 @@ export function App({
history,
]);
+ const getLastKnownDocWithoutPinnedFilters = useCallback(
+ function () {
+ if (!lastKnownDoc) return undefined;
+ const [pinnedFilters, appFilters] = _.partition(
+ injectFilterReferences(lastKnownDoc.state?.filters || [], lastKnownDoc.references),
+ esFilters.isFilterPinned
+ );
+ return pinnedFilters?.length
+ ? {
+ ...lastKnownDoc,
+ state: {
+ ...lastKnownDoc.state,
+ filters: appFilters,
+ },
+ }
+ : lastKnownDoc;
+ },
+ [lastKnownDoc]
+ );
+
useEffect(() => {
onAppLeave((actions) => {
// Confirm when the user has made any changes to an existing doc
// or when the user has configured something without saving
if (
core.application.capabilities.visualize.save &&
- (state.persistedDoc?.expression
- ? !_.isEqual(lastKnownDoc?.expression, state.persistedDoc.expression)
- : lastKnownDoc?.expression)
+ !_.isEqual(state.persistedDoc?.state, getLastKnownDocWithoutPinnedFilters()?.state) &&
+ (state.isSaveable || state.persistedDoc)
) {
return actions.confirm(
i18n.translate('xpack.lens.app.unsavedWorkMessage', {
@@ -198,7 +215,14 @@ export function App({
return actions.default();
}
});
- }, [lastKnownDoc, onAppLeave, state.persistedDoc, core.application.capabilities.visualize.save]);
+ }, [
+ lastKnownDoc,
+ onAppLeave,
+ state.persistedDoc,
+ state.isSaveable,
+ core.application.capabilities.visualize.save,
+ getLastKnownDocWithoutPinnedFilters,
+ ]);
// Sync Kibana breadcrumbs any time the saved document's title changes
useEffect(() => {
@@ -229,13 +253,17 @@ export function App({
.load(docId)
.then((doc) => {
getAllIndexPatterns(
- doc.state.datasourceMetaData.filterableIndexPatterns,
+ _.uniq(
+ doc.references.filter(({ type }) => type === 'index-pattern').map(({ id }) => id)
+ ),
data.indexPatterns,
core.notifications
)
.then((indexPatterns) => {
// Don't overwrite any pinned filters
- data.query.filterManager.setAppFilters(doc.state.filters);
+ data.query.filterManager.setAppFilters(
+ injectFilterReferences(doc.state.filters, doc.references)
+ );
setState((s) => ({
...s,
isLoading: false,
@@ -245,13 +273,13 @@ export function App({
indexPatternsForTopNav: indexPatterns,
}));
})
- .catch(() => {
+ .catch((e) => {
setState((s) => ({ ...s, isLoading: false }));
redirectTo();
});
})
- .catch(() => {
+ .catch((e) => {
setState((s) => ({ ...s, isLoading: false }));
core.notifications.toasts.addDanger(
@@ -287,22 +315,9 @@ export function App({
if (!lastKnownDoc) {
return;
}
- const [pinnedFilters, appFilters] = _.partition(
- lastKnownDoc.state?.filters,
- esFilters.isFilterPinned
- );
- const lastDocWithoutPinned = pinnedFilters?.length
- ? {
- ...lastKnownDoc,
- state: {
- ...lastKnownDoc.state,
- filters: appFilters,
- },
- }
- : lastKnownDoc;
const doc = {
- ...lastDocWithoutPinned,
+ ...getLastKnownDocWithoutPinnedFilters()!,
description: saveProps.newDescription,
id: saveProps.newCopyOnSave ? undefined : lastKnownDoc.id,
title: saveProps.newTitle,
@@ -392,7 +407,7 @@ export function App({
emphasize: true,
iconType: 'check',
run: () => {
- if (isSaveable && lastKnownDoc) {
+ if (savingPermitted) {
runSave({
newTitle: lastKnownDoc.title,
newCopyOnSave: false,
@@ -402,7 +417,7 @@ export function App({
}
},
testId: 'lnsApp_saveAndReturnButton',
- disableButton: !isSaveable,
+ disableButton: !savingPermitted,
},
]
: []),
@@ -417,12 +432,12 @@ export function App({
}),
emphasize: !state.originatingApp || !lastKnownDoc?.id,
run: () => {
- if (isSaveable && lastKnownDoc) {
+ if (savingPermitted) {
setState((s) => ({ ...s, isSaveModalVisible: true }));
}
},
testId: 'lnsApp_saveButton',
- disableButton: !isSaveable,
+ disableButton: !savingPermitted,
},
]}
data-test-subj="lnsApp_topNav"
@@ -503,7 +518,10 @@ export function App({
doc: state.persistedDoc,
onError,
showNoDataPopover,
- onChange: ({ filterableIndexPatterns, doc }) => {
+ onChange: ({ filterableIndexPatterns, doc, isSaveable }) => {
+ if (isSaveable !== state.isSaveable) {
+ setState((s) => ({ ...s, isSaveable }));
+ }
if (!_.isEqual(state.persistedDoc, doc)) {
setState((s) => ({ ...s, lastKnownDoc: doc }));
}
@@ -511,8 +529,8 @@ export function App({
// Update the cached index patterns if the user made a change to any of them
if (
state.indexPatternsForTopNav.length !== filterableIndexPatterns.length ||
- filterableIndexPatterns.find(
- ({ id }) =>
+ filterableIndexPatterns.some(
+ (id) =>
!state.indexPatternsForTopNav.find((indexPattern) => indexPattern.id === id)
)
) {
@@ -554,12 +572,12 @@ export function App({
}
export async function getAllIndexPatterns(
- ids: Array<{ id: string }>,
+ ids: string[],
indexPatternsService: IndexPatternsContract,
notifications: NotificationsStart
): Promise {
try {
- return await Promise.all(ids.map(({ id }) => indexPatternsService.get(id)));
+ return await Promise.all(ids.map((id) => indexPatternsService.get(id)));
} catch (e) {
notifications.toasts.addDanger(
i18n.translate('xpack.lens.app.indexPatternLoadingError', {
diff --git a/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx b/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx
index 0b6584277ffa7..194f12cf9291b 100644
--- a/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx
+++ b/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx
@@ -50,20 +50,6 @@ describe('Datatable Visualization', () => {
});
});
- describe('#getPersistableState', () => {
- it('should persist the internal state', () => {
- const expectedState: DatatableVisualizationState = {
- layers: [
- {
- layerId: 'baz',
- columns: ['a', 'b', 'c'],
- },
- ],
- };
- expect(datatableVisualization.getPersistableState(expectedState)).toEqual(expectedState);
- });
- });
-
describe('#getLayerIds', () => {
it('return the layer ids', () => {
const state: DatatableVisualizationState = {
@@ -340,7 +326,10 @@ describe('Datatable Visualization', () => {
label: 'label',
});
- const expression = datatableVisualization.toExpression({ layers: [layer] }, frame) as Ast;
+ const expression = datatableVisualization.toExpression(
+ { layers: [layer] },
+ frame.datasourceLayers
+ ) as Ast;
const tableArgs = buildExpression(expression).findFunction('lens_datatable_columns');
expect(tableArgs).toHaveLength(1);
diff --git a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx
index 659f8ea12bcb0..5aff4e14b17f2 100644
--- a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx
+++ b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx
@@ -25,10 +25,7 @@ function newLayerState(layerId: string): LayerState {
};
}
-export const datatableVisualization: Visualization<
- DatatableVisualizationState,
- DatatableVisualizationState
-> = {
+export const datatableVisualization: Visualization = {
id: 'lnsDatatable',
visualizationTypes: [
@@ -75,8 +72,6 @@ export const datatableVisualization: Visualization<
);
},
- getPersistableState: (state) => state,
-
getSuggestions({
table,
state,
@@ -186,9 +181,9 @@ export const datatableVisualization: Visualization<
};
},
- toExpression(state, frame): Ast {
+ toExpression(state, datasourceLayers): Ast {
const layer = state.layers[0];
- const datasource = frame.datasourceLayers[layer.layerId];
+ const datasource = datasourceLayers[layer.layerId];
const originalOrder = datasource.getTableSpec().map(({ columnId }) => columnId);
// When we add a column it could be empty, and therefore have no order
const sortedColumns = Array.from(new Set(originalOrder.concat(layer.columns)));
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/__mocks__/expression_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/__mocks__/expression_helpers.ts
new file mode 100644
index 0000000000000..e0b3616315cbd
--- /dev/null
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/__mocks__/expression_helpers.ts
@@ -0,0 +1,14 @@
+/*
+ * 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 { Ast } from '@kbn/interpreter/common';
+
+export function buildExpression(): Ast {
+ return {
+ type: 'expression',
+ chain: [{ type: 'function', function: 'test', arguments: {} }],
+ };
+}
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx
index 38224bf962a3f..b2804cfddba58 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx
@@ -124,7 +124,6 @@ export function LayerPanel(
const nextPublicAPI = layerDatasource.getPublicAPI({
state: newState,
layerId,
- dateRange: props.framePublicAPI.dateRange,
});
const nextTable = new Set(
nextPublicAPI.getTableSpec().map(({ columnId }) => columnId)
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx
index 2f7a78197b2b2..e628ea0675a8d 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx
@@ -170,25 +170,22 @@ describe('editor_frame', () => {
doc={{
visualizationType: 'testVis',
title: '',
- expression: '',
state: {
datasourceStates: {
testDatasource: datasource1State,
testDatasource2: datasource2State,
},
visualization: {},
- datasourceMetaData: {
- filterableIndexPatterns: [],
- },
query: { query: '', language: 'lucene' },
filters: [],
},
+ references: [],
}}
/>
);
});
- expect(mockDatasource.initialize).toHaveBeenCalledWith(datasource1State);
- expect(mockDatasource2.initialize).toHaveBeenCalledWith(datasource2State);
+ expect(mockDatasource.initialize).toHaveBeenCalledWith(datasource1State, []);
+ expect(mockDatasource2.initialize).toHaveBeenCalledWith(datasource2State, []);
expect(mockDatasource3.initialize).not.toHaveBeenCalled();
});
@@ -425,21 +422,6 @@ describe('editor_frame', () => {
"function": "kibana",
"type": "function",
},
- Object {
- "arguments": Object {
- "filters": Array [
- "[]",
- ],
- "query": Array [
- "{\\"query\\":\\"\\",\\"language\\":\\"lucene\\"}",
- ],
- "timeRange": Array [
- "{\\"from\\":\\"\\",\\"to\\":\\"\\"}",
- ],
- },
- "function": "kibana_context",
- "type": "function",
- },
Object {
"arguments": Object {
"layerIds": Array [
@@ -499,19 +481,16 @@ describe('editor_frame', () => {
doc={{
visualizationType: 'testVis',
title: '',
- expression: '',
state: {
datasourceStates: {
testDatasource: {},
testDatasource2: {},
},
visualization: {},
- datasourceMetaData: {
- filterableIndexPatterns: [],
- },
query: { query: '', language: 'lucene' },
filters: [],
},
+ references: [],
}}
/>
);
@@ -535,21 +514,6 @@ describe('editor_frame', () => {
"function": "kibana",
"type": "function",
},
- Object {
- "arguments": Object {
- "filters": Array [
- "[]",
- ],
- "query": Array [
- "{\\"query\\":\\"\\",\\"language\\":\\"lucene\\"}",
- ],
- "timeRange": Array [
- "{\\"from\\":\\"\\",\\"to\\":\\"\\"}",
- ],
- },
- "function": "kibana_context",
- "type": "function",
- },
Object {
"arguments": Object {
"layerIds": Array [
@@ -747,19 +711,16 @@ describe('editor_frame', () => {
doc={{
visualizationType: 'testVis',
title: '',
- expression: '',
state: {
datasourceStates: {
testDatasource: {},
testDatasource2: {},
},
visualization: {},
- datasourceMetaData: {
- filterableIndexPatterns: [],
- },
query: { query: '', language: 'lucene' },
filters: [],
},
+ references: [],
}}
/>
);
@@ -802,19 +763,16 @@ describe('editor_frame', () => {
doc={{
visualizationType: 'testVis',
title: '',
- expression: '',
state: {
datasourceStates: {
testDatasource: datasource1State,
testDatasource2: datasource2State,
},
visualization: {},
- datasourceMetaData: {
- filterableIndexPatterns: [],
- },
query: { query: '', language: 'lucene' },
filters: [],
},
+ references: [],
}}
/>
);
@@ -842,7 +800,6 @@ describe('editor_frame', () => {
it('should give access to the datasource state in the datasource factory function', async () => {
const datasourceState = {};
- const dateRange = { fromDate: 'now-1w', toDate: 'now' };
mockDatasource.initialize.mockResolvedValue(datasourceState);
mockDatasource.getLayers.mockReturnValue(['first']);
@@ -850,7 +807,6 @@ describe('editor_frame', () => {
mount(
{
});
expect(mockDatasource.getPublicAPI).toHaveBeenCalledWith({
- dateRange,
state: datasourceState,
layerId: 'first',
});
@@ -1460,9 +1415,10 @@ describe('editor_frame', () => {
})
);
mockDatasource.getLayers.mockReturnValue(['first']);
- mockDatasource.getMetaData.mockReturnValue({
- filterableIndexPatterns: [{ id: '1', title: 'resolved' }],
- });
+ mockDatasource.getPersistableState = jest.fn((x) => ({
+ state: x,
+ savedObjectReferences: [{ type: 'index-pattern', id: '1', name: 'index-pattern-0' }],
+ }));
mockVisualization.initialize.mockReturnValue({ initialState: true });
await act(async () => {
@@ -1487,14 +1443,20 @@ describe('editor_frame', () => {
expect(onChange).toHaveBeenCalledTimes(2);
expect(onChange).toHaveBeenNthCalledWith(1, {
- filterableIndexPatterns: [{ id: '1', title: 'resolved' }],
+ filterableIndexPatterns: ['1'],
doc: {
- expression: '',
id: undefined,
+ description: undefined,
+ references: [
+ {
+ id: '1',
+ name: 'index-pattern-0',
+ type: 'index-pattern',
+ },
+ ],
state: {
visualization: null, // Not yet loaded
- datasourceMetaData: { filterableIndexPatterns: [{ id: '1', title: 'resolved' }] },
- datasourceStates: { testDatasource: undefined },
+ datasourceStates: { testDatasource: {} },
query: { query: '', language: 'lucene' },
filters: [],
},
@@ -1502,18 +1464,23 @@ describe('editor_frame', () => {
type: 'lens',
visualizationType: 'testVis',
},
+ isSaveable: false,
});
expect(onChange).toHaveBeenLastCalledWith({
- filterableIndexPatterns: [{ id: '1', title: 'resolved' }],
+ filterableIndexPatterns: ['1'],
doc: {
- expression: '',
+ references: [
+ {
+ id: '1',
+ name: 'index-pattern-0',
+ type: 'index-pattern',
+ },
+ ],
+ description: undefined,
id: undefined,
state: {
visualization: { initialState: true }, // Now loaded
- datasourceMetaData: {
- filterableIndexPatterns: [{ id: '1', title: 'resolved' }],
- },
- datasourceStates: { testDatasource: undefined },
+ datasourceStates: { testDatasource: {} },
query: { query: '', language: 'lucene' },
filters: [],
},
@@ -1521,6 +1488,7 @@ describe('editor_frame', () => {
type: 'lens',
visualizationType: 'testVis',
},
+ isSaveable: false,
});
});
@@ -1562,11 +1530,10 @@ describe('editor_frame', () => {
expect(onChange).toHaveBeenNthCalledWith(3, {
filterableIndexPatterns: [],
doc: {
- expression: expect.stringContaining('vis "expression"'),
id: undefined,
+ references: [],
state: {
- datasourceMetaData: { filterableIndexPatterns: [] },
- datasourceStates: { testDatasource: undefined },
+ datasourceStates: { testDatasource: { datasource: '' } },
visualization: { initialState: true },
query: { query: 'new query', language: 'lucene' },
filters: [],
@@ -1575,6 +1542,7 @@ describe('editor_frame', () => {
type: 'lens',
visualizationType: 'testVis',
},
+ isSaveable: true,
});
});
@@ -1583,9 +1551,10 @@ describe('editor_frame', () => {
mockDatasource.initialize.mockResolvedValue({});
mockDatasource.getLayers.mockReturnValue(['first']);
- mockDatasource.getMetaData.mockReturnValue({
- filterableIndexPatterns: [{ id: '1', title: 'resolved' }],
- });
+ mockDatasource.getPersistableState = jest.fn((x) => ({
+ state: x,
+ savedObjectReferences: [{ type: 'index-pattern', id: '1', name: '' }],
+ }));
mockVisualization.initialize.mockReturnValue({ initialState: true });
await act(async () => {
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx
index 48a3511a8f359..72ad8e074226c 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx
@@ -7,13 +7,7 @@
import React, { useEffect, useReducer } from 'react';
import { CoreSetup, CoreStart } from 'kibana/public';
import { ReactExpressionRendererType } from '../../../../../../src/plugins/expressions/public';
-import {
- Datasource,
- DatasourcePublicAPI,
- FramePublicAPI,
- Visualization,
- DatasourceMetaData,
-} from '../../types';
+import { Datasource, FramePublicAPI, Visualization } from '../../types';
import { reducer, getInitialState } from './state_management';
import { DataPanelWrapper } from './data_panel_wrapper';
import { ConfigPanelWrapper } from './config_panel';
@@ -26,6 +20,7 @@ import { getSavedObjectFormat } from './save';
import { generateId } from '../../id_generator';
import { Filter, Query, SavedQuery } from '../../../../../../src/plugins/data/public';
import { EditorFrameStartPlugins } from '../service';
+import { initializeDatasources, createDatasourceLayers } from './state_helpers';
export interface EditorFrameProps {
doc?: Document;
@@ -45,8 +40,9 @@ export interface EditorFrameProps {
filters: Filter[];
savedQuery?: SavedQuery;
onChange: (arg: {
- filterableIndexPatterns: DatasourceMetaData['filterableIndexPatterns'];
+ filterableIndexPatterns: string[];
doc: Document;
+ isSaveable: boolean;
}) => void;
showNoDataPopover: () => void;
}
@@ -67,25 +63,19 @@ export function EditorFrame(props: EditorFrameProps) {
// prevents executing dispatch on unmounted component
let isUnmounted = false;
if (!allLoaded) {
- Object.entries(props.datasourceMap).forEach(([datasourceId, datasource]) => {
- if (
- state.datasourceStates[datasourceId] &&
- state.datasourceStates[datasourceId].isLoading
- ) {
- datasource
- .initialize(state.datasourceStates[datasourceId].state || undefined)
- .then((datasourceState) => {
- if (!isUnmounted) {
- dispatch({
- type: 'UPDATE_DATASOURCE_STATE',
- updater: datasourceState,
- datasourceId,
- });
- }
- })
- .catch(onError);
- }
- });
+ initializeDatasources(props.datasourceMap, state.datasourceStates, props.doc?.references)
+ .then((result) => {
+ if (!isUnmounted) {
+ Object.entries(result).forEach(([datasourceId, { state: datasourceState }]) => {
+ dispatch({
+ type: 'UPDATE_DATASOURCE_STATE',
+ updater: datasourceState,
+ datasourceId,
+ });
+ });
+ }
+ })
+ .catch(onError);
}
return () => {
isUnmounted = true;
@@ -95,22 +85,7 @@ export function EditorFrame(props: EditorFrameProps) {
[allLoaded, onError]
);
- const datasourceLayers: Record = {};
- Object.keys(props.datasourceMap)
- .filter((id) => state.datasourceStates[id] && !state.datasourceStates[id].isLoading)
- .forEach((id) => {
- const datasourceState = state.datasourceStates[id].state;
- const datasource = props.datasourceMap[id];
-
- const layers = datasource.getLayers(datasourceState);
- layers.forEach((layer) => {
- datasourceLayers[layer] = props.datasourceMap[id].getPublicAPI({
- state: datasourceState,
- layerId: layer,
- dateRange: props.dateRange,
- });
- });
- });
+ const datasourceLayers = createDatasourceLayers(props.datasourceMap, state.datasourceStates);
const framePublicAPI: FramePublicAPI = {
datasourceLayers,
@@ -165,7 +140,18 @@ export function EditorFrame(props: EditorFrameProps) {
if (props.doc) {
dispatch({
type: 'VISUALIZATION_LOADED',
- doc: props.doc,
+ doc: {
+ ...props.doc,
+ state: {
+ ...props.doc.state,
+ visualization: props.doc.visualizationType
+ ? props.visualizationMap[props.doc.visualizationType].initialize(
+ framePublicAPI,
+ props.doc.state.visualization
+ )
+ : props.doc.state.visualization,
+ },
+ },
});
} else {
dispatch({
@@ -206,36 +192,20 @@ export function EditorFrame(props: EditorFrameProps) {
return;
}
- const indexPatterns: DatasourceMetaData['filterableIndexPatterns'] = [];
- Object.entries(props.datasourceMap)
- .filter(([id, datasource]) => {
- const stateWrapper = state.datasourceStates[id];
- return (
- stateWrapper &&
- !stateWrapper.isLoading &&
- datasource.getLayers(stateWrapper.state).length > 0
- );
+ props.onChange(
+ getSavedObjectFormat({
+ activeDatasources: Object.keys(state.datasourceStates).reduce(
+ (datasourceMap, datasourceId) => ({
+ ...datasourceMap,
+ [datasourceId]: props.datasourceMap[datasourceId],
+ }),
+ {}
+ ),
+ visualization: activeVisualization,
+ state,
+ framePublicAPI,
})
- .forEach(([id, datasource]) => {
- indexPatterns.push(
- ...datasource.getMetaData(state.datasourceStates[id].state).filterableIndexPatterns
- );
- });
-
- const doc = getSavedObjectFormat({
- activeDatasources: Object.keys(state.datasourceStates).reduce(
- (datasourceMap, datasourceId) => ({
- ...datasourceMap,
- [datasourceId]: props.datasourceMap[datasourceId],
- }),
- {}
- ),
- visualization: activeVisualization,
- state,
- framePublicAPI,
- });
-
- props.onChange({ filterableIndexPatterns: indexPatterns, doc });
+ );
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/expression_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/expression_helpers.ts
index ee28ccfe1bf53..952718e13c8cf 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/expression_helpers.ts
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/expression_helpers.ts
@@ -5,8 +5,7 @@
*/
import { Ast, fromExpression, ExpressionFunctionAST } from '@kbn/interpreter/common';
-import { Visualization, Datasource, FramePublicAPI } from '../../types';
-import { Filter, TimeRange, Query } from '../../../../../../src/plugins/data/public';
+import { Visualization, Datasource, DatasourcePublicAPI } from '../../types';
export function prependDatasourceExpression(
visualizationExpression: Ast | string | null,
@@ -58,40 +57,12 @@ export function prependDatasourceExpression(
? fromExpression(visualizationExpression)
: visualizationExpression;
- return {
- type: 'expression',
- chain: [datafetchExpression, ...parsedVisualizationExpression.chain],
- };
-}
-
-export function prependKibanaContext(
- expression: Ast | string,
- {
- timeRange,
- query,
- filters,
- }: {
- timeRange?: TimeRange;
- query?: Query;
- filters?: Filter[];
- }
-): Ast {
- const parsedExpression = typeof expression === 'string' ? fromExpression(expression) : expression;
-
return {
type: 'expression',
chain: [
{ type: 'function', function: 'kibana', arguments: {} },
- {
- type: 'function',
- function: 'kibana_context',
- arguments: {
- timeRange: timeRange ? [JSON.stringify(timeRange)] : [],
- query: query ? [JSON.stringify(query)] : [],
- filters: [JSON.stringify(filters || [])],
- },
- },
- ...parsedExpression.chain,
+ datafetchExpression,
+ ...parsedVisualizationExpression.chain,
],
};
}
@@ -101,8 +72,7 @@ export function buildExpression({
visualizationState,
datasourceMap,
datasourceStates,
- framePublicAPI,
- removeDateRange,
+ datasourceLayers,
}: {
visualization: Visualization | null;
visualizationState: unknown;
@@ -114,24 +84,12 @@ export function buildExpression({
state: unknown;
}
>;
- framePublicAPI: FramePublicAPI;
- removeDateRange?: boolean;
+ datasourceLayers: Record;
}): Ast | null {
if (visualization === null) {
return null;
}
- const visualizationExpression = visualization.toExpression(visualizationState, framePublicAPI);
-
- const expressionContext = removeDateRange
- ? { query: framePublicAPI.query, filters: framePublicAPI.filters }
- : {
- query: framePublicAPI.query,
- timeRange: {
- from: framePublicAPI.dateRange.fromDate,
- to: framePublicAPI.dateRange.toDate,
- },
- filters: framePublicAPI.filters,
- };
+ const visualizationExpression = visualization.toExpression(visualizationState, datasourceLayers);
const completeExpression = prependDatasourceExpression(
visualizationExpression,
@@ -139,9 +97,5 @@ export function buildExpression({
datasourceStates
);
- if (completeExpression) {
- return prependKibanaContext(completeExpression, expressionContext);
- } else {
- return null;
- }
+ return completeExpression;
}
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/save.test.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/save.test.ts
index d72e5c57ce56e..45d24fd30e2fc 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/save.test.ts
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/save.test.ts
@@ -8,14 +8,18 @@ import { getSavedObjectFormat, Props } from './save';
import { createMockDatasource, createMockVisualization } from '../mocks';
import { esFilters, IIndexPattern, IFieldType } from '../../../../../../src/plugins/data/public';
+jest.mock('./expression_helpers');
+
describe('save editor frame state', () => {
const mockVisualization = createMockVisualization();
- mockVisualization.getPersistableState.mockImplementation((x) => x);
const mockDatasource = createMockDatasource('a');
const mockIndexPattern = ({ id: 'indexpattern' } as unknown) as IIndexPattern;
const mockField = ({ name: '@timestamp' } as unknown) as IFieldType;
- mockDatasource.getPersistableState.mockImplementation((x) => x);
+ mockDatasource.getPersistableState.mockImplementation((x) => ({
+ state: x,
+ savedObjectReferences: [],
+ }));
const saveArgs: Props = {
activeDatasources: {
indexpattern: mockDatasource,
@@ -47,15 +51,17 @@ describe('save editor frame state', () => {
it('transforms from internal state to persisted doc format', async () => {
const datasource = createMockDatasource('a');
datasource.getPersistableState.mockImplementation((state) => ({
- stuff: `${state}_datasource_persisted`,
+ state: {
+ stuff: `${state}_datasource_persisted`,
+ },
+ savedObjectReferences: [],
}));
+ datasource.toExpression.mockReturnValue('my | expr');
const visualization = createMockVisualization();
- visualization.getPersistableState.mockImplementation((state) => ({
- things: `${state}_vis_persisted`,
- }));
+ visualization.toExpression.mockReturnValue('vis | expr');
- const doc = await getSavedObjectFormat({
+ const { doc, filterableIndexPatterns, isSaveable } = await getSavedObjectFormat({
...saveArgs,
activeDatasources: {
indexpattern: datasource,
@@ -74,27 +80,32 @@ describe('save editor frame state', () => {
visualization,
});
+ expect(filterableIndexPatterns).toEqual([]);
+ expect(isSaveable).toEqual(true);
expect(doc).toEqual({
id: undefined,
- expression: '',
state: {
- datasourceMetaData: {
- filterableIndexPatterns: [],
- },
datasourceStates: {
indexpattern: {
stuff: '2_datasource_persisted',
},
},
- visualization: { things: '4_vis_persisted' },
+ visualization: '4',
query: { query: '', language: 'lucene' },
filters: [
{
- meta: { index: 'indexpattern' },
+ meta: { indexRefName: 'filter-index-pattern-0' },
exists: { field: '@timestamp' },
},
],
},
+ references: [
+ {
+ id: 'indexpattern',
+ name: 'filter-index-pattern-0',
+ type: 'index-pattern',
+ },
+ ],
title: 'bbb',
type: 'lens',
visualizationType: '3',
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/save.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/save.ts
index b41e93def966e..6da6d5a8c118f 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/save.ts
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/save.ts
@@ -5,11 +5,12 @@
*/
import _ from 'lodash';
-import { toExpression } from '@kbn/interpreter/target/common';
+import { SavedObjectReference } from 'kibana/public';
import { EditorFrameState } from './state_management';
import { Document } from '../../persistence/saved_object_store';
-import { buildExpression } from './expression_helpers';
import { Datasource, Visualization, FramePublicAPI } from '../../types';
+import { extractFilterReferences } from '../../persistence';
+import { buildExpression } from './expression_helpers';
export interface Props {
activeDatasources: Record;
@@ -23,43 +24,55 @@ export function getSavedObjectFormat({
state,
visualization,
framePublicAPI,
-}: Props): Document {
+}: Props): {
+ doc: Document;
+ filterableIndexPatterns: string[];
+ isSaveable: boolean;
+} {
+ const datasourceStates: Record = {};
+ const references: SavedObjectReference[] = [];
+ Object.entries(activeDatasources).forEach(([id, datasource]) => {
+ const { state: persistableState, savedObjectReferences } = datasource.getPersistableState(
+ state.datasourceStates[id].state
+ );
+ datasourceStates[id] = persistableState;
+ references.push(...savedObjectReferences);
+ });
+
+ const uniqueFilterableIndexPatternIds = _.uniq(
+ references.filter(({ type }) => type === 'index-pattern').map(({ id }) => id)
+ );
+
+ const { persistableFilters, references: filterReferences } = extractFilterReferences(
+ framePublicAPI.filters
+ );
+
+ references.push(...filterReferences);
+
const expression = buildExpression({
visualization,
visualizationState: state.visualization.state,
datasourceMap: activeDatasources,
datasourceStates: state.datasourceStates,
- framePublicAPI,
- removeDateRange: true,
- });
-
- const datasourceStates: Record = {};
- Object.entries(activeDatasources).forEach(([id, datasource]) => {
- datasourceStates[id] = datasource.getPersistableState(state.datasourceStates[id].state);
- });
-
- const filterableIndexPatterns: Array<{ id: string; title: string }> = [];
- Object.entries(activeDatasources).forEach(([id, datasource]) => {
- filterableIndexPatterns.push(
- ...datasource.getMetaData(state.datasourceStates[id].state).filterableIndexPatterns
- );
+ datasourceLayers: framePublicAPI.datasourceLayers,
});
return {
- id: state.persistedId,
- title: state.title,
- description: state.description,
- type: 'lens',
- visualizationType: state.visualization.activeId,
- expression: expression ? toExpression(expression) : '',
- state: {
- datasourceStates,
- datasourceMetaData: {
- filterableIndexPatterns: _.uniqBy(filterableIndexPatterns, 'id'),
+ doc: {
+ id: state.persistedId,
+ title: state.title,
+ description: state.description,
+ type: 'lens',
+ visualizationType: state.visualization.activeId,
+ state: {
+ datasourceStates,
+ visualization: state.visualization.state,
+ query: framePublicAPI.query,
+ filters: persistableFilters,
},
- visualization: visualization.getPersistableState(state.visualization.state),
- query: framePublicAPI.query,
- filters: framePublicAPI.filters,
+ references,
},
+ filterableIndexPatterns: uniqueFilterableIndexPatternIds,
+ isSaveable: expression !== null,
};
}
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts
new file mode 100644
index 0000000000000..6deb9ffd37a06
--- /dev/null
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts
@@ -0,0 +1,87 @@
+/*
+ * 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 { SavedObjectReference } from 'kibana/public';
+import { Ast } from '@kbn/interpreter/common';
+import { Datasource, DatasourcePublicAPI, Visualization } from '../../types';
+import { buildExpression } from './expression_helpers';
+import { Document } from '../../persistence/saved_object_store';
+
+export async function initializeDatasources(
+ datasourceMap: Record,
+ datasourceStates: Record,
+ references?: SavedObjectReference[]
+) {
+ const states: Record = {};
+ await Promise.all(
+ Object.entries(datasourceMap).map(([datasourceId, datasource]) => {
+ if (datasourceStates[datasourceId]) {
+ return datasource
+ .initialize(datasourceStates[datasourceId].state || undefined, references)
+ .then((datasourceState) => {
+ states[datasourceId] = { isLoading: false, state: datasourceState };
+ });
+ }
+ })
+ );
+ return states;
+}
+
+export function createDatasourceLayers(
+ datasourceMap: Record,
+ datasourceStates: Record
+) {
+ const datasourceLayers: Record = {};
+ Object.keys(datasourceMap)
+ .filter((id) => datasourceStates[id] && !datasourceStates[id].isLoading)
+ .forEach((id) => {
+ const datasourceState = datasourceStates[id].state;
+ const datasource = datasourceMap[id];
+
+ const layers = datasource.getLayers(datasourceState);
+ layers.forEach((layer) => {
+ datasourceLayers[layer] = datasourceMap[id].getPublicAPI({
+ state: datasourceState,
+ layerId: layer,
+ });
+ });
+ });
+ return datasourceLayers;
+}
+
+export async function persistedStateToExpression(
+ datasources: Record,
+ visualizations: Record,
+ doc: Document
+): Promise {
+ const {
+ state: { visualization: visualizationState, datasourceStates: persistedDatasourceStates },
+ visualizationType,
+ references,
+ } = doc;
+ if (!visualizationType) return null;
+ const visualization = visualizations[visualizationType!];
+ const datasourceStates = await initializeDatasources(
+ datasources,
+ Object.fromEntries(
+ Object.entries(persistedDatasourceStates).map(([id, state]) => [
+ id,
+ { isLoading: false, state },
+ ])
+ ),
+ references
+ );
+
+ const datasourceLayers = createDatasourceLayers(datasources, datasourceStates);
+
+ return buildExpression({
+ visualization,
+ visualizationState,
+ datasourceMap: datasources,
+ datasourceStates,
+ datasourceLayers,
+ });
+}
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.test.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.test.ts
index 969467b5789ec..c7f505aeca517 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.test.ts
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.test.ts
@@ -57,19 +57,16 @@ describe('editor_frame state management', () => {
const initialState = getInitialState({
...props,
doc: {
- expression: '',
state: {
datasourceStates: {
testDatasource: { internalState1: '' },
testDatasource2: { internalState2: '' },
},
visualization: {},
- datasourceMetaData: {
- filterableIndexPatterns: [],
- },
query: { query: '', language: 'lucene' },
filters: [],
},
+ references: [],
title: '',
visualizationType: 'testVis',
},
@@ -380,9 +377,7 @@ describe('editor_frame state management', () => {
type: 'VISUALIZATION_LOADED',
doc: {
id: 'b',
- expression: '',
state: {
- datasourceMetaData: { filterableIndexPatterns: [] },
datasourceStates: { a: { foo: 'c' } },
visualization: { bar: 'd' },
query: { query: '', language: 'lucene' },
@@ -392,6 +387,7 @@ describe('editor_frame state management', () => {
description: 'My lens',
type: 'lens',
visualizationType: 'line',
+ references: [],
},
}
);
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts
index 263f7cd65f43d..2bb1baf9d54f2 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts
@@ -107,7 +107,7 @@ export function getSuggestions({
* title and preview expression.
*/
function getVisualizationSuggestions(
- visualization: Visualization,
+ visualization: Visualization,
table: TableSuggestion,
visualizationId: string,
datasourceSuggestion: DatasourceSuggestion & { datasourceId: string },
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx
index fd509c0046e13..323472d717352 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx
@@ -249,7 +249,6 @@ describe('suggestion_panel', () => {
expect(passedExpression).toMatchInlineSnapshot(`
"kibana
- | kibana_context timeRange=\\"{\\\\\\"from\\\\\\":\\\\\\"now-7d\\\\\\",\\\\\\"to\\\\\\":\\\\\\"now\\\\\\"}\\" query=\\"{\\\\\\"query\\\\\\":\\\\\\"\\\\\\",\\\\\\"language\\\\\\":\\\\\\"lucene\\\\\\"}\\" filters=\\"[{\\\\\\"meta\\\\\\":{\\\\\\"index\\\\\\":\\\\\\"index1\\\\\\"},\\\\\\"exists\\\\\\":{\\\\\\"field\\\\\\":\\\\\\"myfield\\\\\\"}}]\\"
| lens_merge_tables layerIds=\\"first\\" tables={datasource_expression}
| test
| expression"
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx
index 7395075cf9f74..f1dc3fa306d15 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx
@@ -21,6 +21,7 @@ import { IconType } from '@elastic/eui/src/components/icon/icon';
import { Ast, toExpression } from '@kbn/interpreter/common';
import { i18n } from '@kbn/i18n';
import classNames from 'classnames';
+import { ExecutionContextSearch } from 'src/plugins/expressions';
import { Action, PreviewState } from './state_management';
import { Datasource, Visualization, FramePublicAPI, DatasourcePublicAPI } from '../../types';
import { getSuggestions, switchToSuggestion } from './suggestion_helpers';
@@ -28,7 +29,7 @@ import {
ReactExpressionRendererProps,
ReactExpressionRendererType,
} from '../../../../../../src/plugins/expressions/public';
-import { prependDatasourceExpression, prependKibanaContext } from './expression_helpers';
+import { prependDatasourceExpression } from './expression_helpers';
import { debouncedComponent } from '../../debounced_component';
import { trackUiEvent, trackSuggestionEvent } from '../../lens_ui_telemetry';
import { DataPublicPluginStart } from '../../../../../../src/plugins/data/public';
@@ -112,7 +113,7 @@ const SuggestionPreview = ({
}: {
onSelect: () => void;
preview: {
- expression?: Ast;
+ expression?: Ast | null;
icon: IconType;
title: string;
};
@@ -215,12 +216,24 @@ export function SuggestionPanel({
visualizationMap,
]);
+ const context: ExecutionContextSearch = useMemo(
+ () => ({
+ query: frame.query,
+ timeRange: {
+ from: frame.dateRange.fromDate,
+ to: frame.dateRange.toDate,
+ },
+ filters: frame.filters,
+ }),
+ [frame.query, frame.dateRange.fromDate, frame.dateRange.toDate, frame.filters]
+ );
+
const AutoRefreshExpressionRenderer = useMemo(() => {
const autoRefreshFetch$ = plugins.data.query.timefilter.timefilter.getAutoRefreshFetch$();
return (props: ReactExpressionRendererProps) => (
-
+
);
- }, [plugins.data.query.timefilter.timefilter]);
+ }, [plugins.data.query.timefilter.timefilter, context]);
const [lastSelectedSuggestion, setLastSelectedSuggestion] = useState(-1);
@@ -252,15 +265,6 @@ export function SuggestionPanel({
}
}
- const expressionContext = {
- query: frame.query,
- filters: frame.filters,
- timeRange: {
- from: frame.dateRange.fromDate,
- to: frame.dateRange.toDate,
- },
- };
-
return (
@@ -305,9 +309,7 @@ export function SuggestionPanel({
{currentVisualizationId && (
,
+ newVisualization: Visualization,
subVisualizationId?: string
): Suggestion | undefined {
const unfilteredSuggestions = getSuggestions({
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx
index a9c638df8cad1..47e3b41df3b21 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx
@@ -172,21 +172,6 @@ describe('workspace_panel', () => {
"function": "kibana",
"type": "function",
},
- Object {
- "arguments": Object {
- "filters": Array [
- "[]",
- ],
- "query": Array [
- "{\\"query\\":\\"\\",\\"language\\":\\"lucene\\"}",
- ],
- "timeRange": Array [
- "{\\"from\\":\\"now-7d\\",\\"to\\":\\"now\\"}",
- ],
- },
- "function": "kibana_context",
- "type": "function",
- },
Object {
"arguments": Object {
"layerIds": Array [
@@ -305,10 +290,10 @@ describe('workspace_panel', () => {
);
expect(
- (instance.find(expressionRendererMock).prop('expression') as Ast).chain[2].arguments.layerIds
+ (instance.find(expressionRendererMock).prop('expression') as Ast).chain[1].arguments.layerIds
).toEqual(['first', 'second', 'third']);
expect(
- (instance.find(expressionRendererMock).prop('expression') as Ast).chain[2].arguments.tables
+ (instance.find(expressionRendererMock).prop('expression') as Ast).chain[1].arguments.tables
).toMatchInlineSnapshot(`
Array [
Object {
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx
index b3a12271f377b..4f914bc65dc7c 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx
@@ -18,6 +18,7 @@ import {
EuiLink,
} from '@elastic/eui';
import { CoreStart, CoreSetup } from 'kibana/public';
+import { ExecutionContextSearch } from 'src/plugins/expressions';
import {
ExpressionRendererEvent,
ReactExpressionRendererType,
@@ -129,7 +130,7 @@ export function InnerWorkspacePanel({
visualizationState,
datasourceMap,
datasourceStates,
- framePublicAPI,
+ datasourceLayers: framePublicAPI.datasourceLayers,
});
} catch (e) {
// Most likely an error in the expression provided by a datasource or visualization
@@ -173,6 +174,23 @@ export function InnerWorkspacePanel({
[plugins.data.query.timefilter.timefilter]
);
+ const context: ExecutionContextSearch = useMemo(
+ () => ({
+ query: framePublicAPI.query,
+ timeRange: {
+ from: framePublicAPI.dateRange.fromDate,
+ to: framePublicAPI.dateRange.toDate,
+ },
+ filters: framePublicAPI.filters,
+ }),
+ [
+ framePublicAPI.query,
+ framePublicAPI.dateRange.fromDate,
+ framePublicAPI.dateRange.toDate,
+ framePublicAPI.filters,
+ ]
+ );
+
useEffect(() => {
// reset expression error if component attempts to run it again
if (expression && localState.expressionBuildError) {
@@ -264,6 +282,7 @@ export function InnerWorkspacePanel({
className="lnsExpressionRenderer__component"
padding="m"
expression={expression!}
+ searchContext={context}
reload$={autoRefreshFetch$}
onEvent={onEvent}
renderError={(errorMessage?: string | null) => {
diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx
index 69447b3b9a9b8..1e2df28cad7b1 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx
@@ -18,16 +18,13 @@ jest.mock('../../../../../../src/plugins/inspector/public/', () => ({
}));
const savedVis: Document = {
- expression: 'my | expression',
state: {
visualization: {},
datasourceStates: {},
- datasourceMetaData: {
- filterableIndexPatterns: [],
- },
query: { query: '', language: 'lucene' },
filters: [],
},
+ references: [],
title: 'My title',
visualizationType: '',
};
@@ -59,13 +56,14 @@ describe('embeddable', () => {
editUrl: '',
editable: true,
savedVis,
+ expression: 'my | expression',
},
{ id: '123' }
);
embeddable.render(mountpoint);
expect(expressionRenderer).toHaveBeenCalledTimes(1);
- expect(expressionRenderer.mock.calls[0][0]!.expression).toEqual(savedVis.expression);
+ expect(expressionRenderer.mock.calls[0][0]!.expression).toEqual('my | expression');
});
it('should re-render if new input is pushed', () => {
@@ -82,6 +80,7 @@ describe('embeddable', () => {
editUrl: '',
editable: true,
savedVis,
+ expression: 'my | expression',
},
{ id: '123' }
);
@@ -110,6 +109,7 @@ describe('embeddable', () => {
editUrl: '',
editable: true,
savedVis,
+ expression: 'my | expression',
},
{ id: '123', timeRange, query, filters }
);
@@ -117,11 +117,52 @@ describe('embeddable', () => {
expect(expressionRenderer.mock.calls[0][0].searchContext).toEqual({
timeRange,
- query,
+ query: [query, savedVis.state.query],
filters,
});
});
+ it('should merge external context with query and filters of the saved object', () => {
+ const timeRange: TimeRange = { from: 'now-15d', to: 'now' };
+ const query: Query = { language: 'kquery', query: 'external filter' };
+ const filters: Filter[] = [{ meta: { alias: 'test', negate: false, disabled: false } }];
+
+ const embeddable = new Embeddable(
+ dataPluginMock.createSetupContract().query.timefilter.timefilter,
+ expressionRenderer,
+ getTrigger,
+ {
+ editPath: '',
+ editUrl: '',
+ editable: true,
+ savedVis: {
+ ...savedVis,
+ state: {
+ ...savedVis.state,
+ query: { language: 'kquery', query: 'saved filter' },
+ filters: [
+ { meta: { alias: 'test', negate: false, disabled: false, indexRefName: 'filter-0' } },
+ ],
+ },
+ references: [{ type: 'index-pattern', name: 'filter-0', id: 'my-index-pattern-id' }],
+ },
+ expression: 'my | expression',
+ },
+ { id: '123', timeRange, query, filters }
+ );
+ embeddable.render(mountpoint);
+
+ expect(expressionRenderer.mock.calls[0][0].searchContext).toEqual({
+ timeRange,
+ query: [query, { language: 'kquery', query: 'saved filter' }],
+ filters: [
+ filters[0],
+ // actual index pattern id gets injected
+ { meta: { alias: 'test', negate: false, disabled: false, index: 'my-index-pattern-id' } },
+ ],
+ });
+ });
+
it('should execute trigger on event from expression renderer', () => {
const embeddable = new Embeddable(
dataPluginMock.createSetupContract().query.timefilter.timefilter,
@@ -132,6 +173,7 @@ describe('embeddable', () => {
editUrl: '',
editable: true,
savedVis,
+ expression: 'my | expression',
},
{ id: '123' }
);
@@ -162,6 +204,7 @@ describe('embeddable', () => {
editUrl: '',
editable: true,
savedVis,
+ expression: 'my | expression',
},
{ id: '123', timeRange, query, filters }
);
@@ -195,6 +238,7 @@ describe('embeddable', () => {
editUrl: '',
editable: true,
savedVis,
+ expression: 'my | expression',
},
{ id: '123', timeRange, query, filters }
);
diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx
index bbd2b18907e9b..4df218a3e94e9 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx
@@ -14,6 +14,7 @@ import {
TimefilterContract,
TimeRange,
} from 'src/plugins/data/public';
+import { ExecutionContextSearch } from 'src/plugins/expressions';
import { Subscription } from 'rxjs';
import {
@@ -28,12 +29,13 @@ import {
EmbeddableOutput,
IContainer,
} from '../../../../../../src/plugins/embeddable/public';
-import { DOC_TYPE, Document } from '../../persistence';
+import { DOC_TYPE, Document, injectFilterReferences } from '../../persistence';
import { ExpressionWrapper } from './expression_wrapper';
import { UiActionsStart } from '../../../../../../src/plugins/ui_actions/public';
import { isLensBrushEvent, isLensFilterEvent } from '../../types';
export interface LensEmbeddableConfiguration {
+ expression: string | null;
savedVis: Document;
editUrl: string;
editPath: string;
@@ -56,12 +58,13 @@ export class Embeddable extends AbstractEmbeddable this.onContainerStateChanged(input));
this.onContainerStateChanged(initialInput);
@@ -122,14 +133,14 @@ export class Embeddable extends AbstractEmbeddable !filter.meta.disabled)
: undefined;
if (
- !_.isEqual(containerState.timeRange, this.currentContext.timeRange) ||
- !_.isEqual(containerState.query, this.currentContext.query) ||
- !_.isEqual(cleanedFilters, this.currentContext.filters)
+ !_.isEqual(containerState.timeRange, this.externalSearchContext.timeRange) ||
+ !_.isEqual(containerState.query, this.externalSearchContext.query) ||
+ !_.isEqual(cleanedFilters, this.externalSearchContext.filters)
) {
- this.currentContext = {
+ this.externalSearchContext = {
timeRange: containerState.timeRange,
query: containerState.query,
- lastReloadRequestTime: this.currentContext.lastReloadRequestTime,
+ lastReloadRequestTime: this.externalSearchContext.lastReloadRequestTime,
filters: cleanedFilters,
};
@@ -149,14 +160,37 @@ export class Embeddable extends AbstractEmbeddable,
domNode
);
}
+ /**
+ * Combines the embeddable context with the saved object context, and replaces
+ * any references to index patterns
+ */
+ private getMergedSearchContext(): ExecutionContextSearch {
+ const output: ExecutionContextSearch = {
+ timeRange: this.externalSearchContext.timeRange,
+ };
+ if (this.externalSearchContext.query) {
+ output.query = [this.externalSearchContext.query, this.savedVis.state.query];
+ } else {
+ output.query = [this.savedVis.state.query];
+ }
+ if (this.externalSearchContext.filters?.length) {
+ output.filters = [...this.externalSearchContext.filters, ...this.savedVis.state.filters];
+ } else {
+ output.filters = [...this.savedVis.state.filters];
+ }
+
+ output.filters = injectFilterReferences(output.filters, this.savedVis.references);
+ return output;
+ }
+
handleEvent = (event: ExpressionRendererEvent) => {
if (!this.getTrigger || this.input.disableTriggers) {
return;
@@ -188,9 +222,9 @@ export class Embeddable extends AbstractEmbeddable Promise;
}
export class EmbeddableFactory implements EmbeddableFactoryDefinition {
@@ -72,13 +75,15 @@ export class EmbeddableFactory implements EmbeddableFactoryDefinition {
indexPatternService,
timefilter,
expressionRenderer,
+ documentToExpression,
uiActions,
} = await this.getStartServices();
const store = new SavedObjectIndexStore(savedObjectsClient);
const savedVis = await store.load(savedObjectId);
- const promises = savedVis.state.datasourceMetaData.filterableIndexPatterns.map(
- async ({ id }) => {
+ const promises = savedVis.references
+ .filter(({ type }) => type === 'index-pattern')
+ .map(async ({ id }) => {
try {
return await indexPatternService.get(id);
} catch (error) {
@@ -87,14 +92,15 @@ export class EmbeddableFactory implements EmbeddableFactoryDefinition {
// to show.
return null;
}
- }
- );
+ });
const indexPatterns = (
await Promise.all(promises)
).filter((indexPattern: IndexPattern | null): indexPattern is IndexPattern =>
Boolean(indexPattern)
);
+ const expression = await documentToExpression(savedVis);
+
return new Embeddable(
timefilter,
expressionRenderer,
@@ -105,6 +111,7 @@ export class EmbeddableFactory implements EmbeddableFactoryDefinition {
editUrl: coreHttp.basePath.prepend(`/app/lens${getEditPath(savedObjectId)}`),
editable: await this.isEditable(),
indexPatterns,
+ expression: expression ? toExpression(expression) : null,
},
input,
parent
diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx b/x-pack/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx
index 296dcef3e70b9..d0d2360ddc107 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx
@@ -8,28 +8,23 @@ import React from 'react';
import { I18nProvider } from '@kbn/i18n/react';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiFlexGroup, EuiFlexItem, EuiText, EuiIcon } from '@elastic/eui';
-import { TimeRange, Filter, Query } from 'src/plugins/data/public';
import {
ExpressionRendererEvent,
ReactExpressionRendererType,
} from 'src/plugins/expressions/public';
+import { ExecutionContextSearch } from 'src/plugins/expressions';
export interface ExpressionWrapperProps {
ExpressionRenderer: ReactExpressionRendererType;
expression: string | null;
- context: {
- timeRange?: TimeRange;
- query?: Query;
- filters?: Filter[];
- lastReloadRequestTime?: number;
- };
+ searchContext: ExecutionContextSearch;
handleEvent: (event: ExpressionRendererEvent) => void;
}
export function ExpressionWrapper({
ExpressionRenderer: ExpressionRendererComponent,
expression,
- context,
+ searchContext,
handleEvent,
}: ExpressionWrapperProps) {
return (
@@ -54,7 +49,7 @@ export function ExpressionWrapper({
className="lnsExpressionRenderer__component"
padding="m"
expression={expression}
- searchContext={{ ...context }}
+ searchContext={searchContext}
renderError={(error) => {error}
}
onEvent={handleEvent}
/>
diff --git a/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx b/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx
index 9c0825b3c2d27..86b137851d9bd 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx
@@ -31,7 +31,6 @@ export function createMockVisualization(): jest.Mocked {
getVisualizationTypeId: jest.fn((_state) => 'empty'),
getDescription: jest.fn((_state) => ({ label: '' })),
switchVisualizationType: jest.fn((_, x) => x),
- getPersistableState: jest.fn((_state) => _state),
getSuggestions: jest.fn((_options) => []),
initialize: jest.fn((_frame, _state?) => ({})),
getConfiguration: jest.fn((props) => ({
@@ -71,7 +70,7 @@ export function createMockDatasource(id: string): DatasourceMock {
clearLayer: jest.fn((state, _layerId) => state),
getDatasourceSuggestionsForField: jest.fn((_state, _item) => []),
getDatasourceSuggestionsFromCurrentState: jest.fn((_state) => []),
- getPersistableState: jest.fn(),
+ getPersistableState: jest.fn((x) => ({ state: x, savedObjectReferences: [] })),
getPublicAPI: jest.fn().mockReturnValue(publicAPIMock),
initialize: jest.fn((_state?) => Promise.resolve()),
renderDataPanel: jest.fn(),
@@ -81,7 +80,6 @@ export function createMockDatasource(id: string): DatasourceMock {
removeLayer: jest.fn((_state, _layerId) => {}),
removeColumn: jest.fn((props) => {}),
getLayers: jest.fn((_state) => []),
- getMetaData: jest.fn((_state) => ({ filterableIndexPatterns: [] })),
renderDimensionTrigger: jest.fn(),
renderDimensionEditor: jest.fn(),
diff --git a/x-pack/plugins/lens/public/editor_frame_service/service.tsx b/x-pack/plugins/lens/public/editor_frame_service/service.tsx
index 47339373b6d1a..5fc347179a032 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/service.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/service.tsx
@@ -21,12 +21,14 @@ import {
EditorFrameInstance,
EditorFrameStart,
} from '../types';
+import { Document } from '../persistence/saved_object_store';
import { EditorFrame } from './editor_frame';
import { mergeTables } from './merge_tables';
import { formatColumn } from './format_column';
import { EmbeddableFactory } from './embeddable/embeddable_factory';
import { getActiveDatasourceIdFromDoc } from './editor_frame/state_management';
import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public';
+import { persistedStateToExpression } from './editor_frame/state_helpers';
export interface EditorFrameSetupPlugins {
data: DataPublicPluginSetup;
@@ -59,6 +61,21 @@ export class EditorFrameService {
private readonly datasources: Array> = [];
private readonly visualizations: Array> = [];
+ /**
+ * This method takes a Lens saved object as returned from the persistence helper,
+ * initializes datsources and visualization and creates the current expression.
+ * This is an asynchronous process and should only be triggered once for a saved object.
+ * @param doc parsed Lens saved object
+ */
+ private async documentToExpression(doc: Document) {
+ const [resolvedDatasources, resolvedVisualizations] = await Promise.all([
+ collectAsyncDefinitions(this.datasources),
+ collectAsyncDefinitions(this.visualizations),
+ ]);
+
+ return await persistedStateToExpression(resolvedDatasources, resolvedVisualizations, doc);
+ }
+
public setup(
core: CoreSetup,
plugins: EditorFrameSetupPlugins
@@ -74,6 +91,7 @@ export class EditorFrameService {
coreHttp: coreStart.http,
timefilter: deps.data.query.timefilter.timefilter,
expressionRenderer: deps.expressions.ReactExpressionRenderer,
+ documentToExpression: this.documentToExpression.bind(this),
indexPatternService: deps.data.indexPatterns,
uiActions: deps.uiActions,
};
@@ -88,7 +106,7 @@ export class EditorFrameService {
this.datasources.push(datasource as Datasource);
},
registerVisualization: (visualization) => {
- this.visualizations.push(visualization as Visualization);
+ this.visualizations.push(visualization as Visualization);
},
};
}
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/__mocks__/loader.ts b/x-pack/plugins/lens/public/indexpattern_datasource/__mocks__/loader.ts
index ca5fe706985f8..c487e31f5a973 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/__mocks__/loader.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/__mocks__/loader.ts
@@ -23,3 +23,7 @@ export function loadInitialState() {
};
return result;
}
+
+const originalLoader = jest.requireActual('../loader');
+
+export const extractReferences = originalLoader.extractReferences;
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts
index dc3938ce436e5..0ba7b7df97853 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts
@@ -128,12 +128,15 @@ const expectedIndexPatterns = {
},
};
-function stateFromPersistedState(
- persistedState: IndexPatternPersistedState
-): IndexPatternPrivateState {
+type IndexPatternBaseState = Omit<
+ IndexPatternPrivateState,
+ 'indexPatternRefs' | 'indexPatterns' | 'existingFields' | 'isFirstExistenceFetch'
+>;
+
+function enrichBaseState(baseState: IndexPatternBaseState): IndexPatternPrivateState {
return {
- currentIndexPatternId: persistedState.currentIndexPatternId,
- layers: persistedState.layers,
+ currentIndexPatternId: baseState.currentIndexPatternId,
+ layers: baseState.layers,
indexPatterns: expectedIndexPatterns,
indexPatternRefs: [],
existingFields: {},
@@ -142,7 +145,10 @@ function stateFromPersistedState(
}
describe('IndexPattern Data Source', () => {
- let persistedState: IndexPatternPersistedState;
+ let baseState: Omit<
+ IndexPatternPrivateState,
+ 'indexPatternRefs' | 'indexPatterns' | 'existingFields' | 'isFirstExistenceFetch'
+ >;
let indexPatternDatasource: Datasource;
beforeEach(() => {
@@ -153,7 +159,7 @@ describe('IndexPattern Data Source', () => {
charts: chartPluginMock.createSetupContract(),
});
- persistedState = {
+ baseState = {
currentIndexPatternId: '1',
layers: {
first: {
@@ -224,9 +230,37 @@ describe('IndexPattern Data Source', () => {
describe('#getPersistedState', () => {
it('should persist from saved state', async () => {
- const state = stateFromPersistedState(persistedState);
+ const state = enrichBaseState(baseState);
- expect(indexPatternDatasource.getPersistableState(state)).toEqual(persistedState);
+ expect(indexPatternDatasource.getPersistableState(state)).toEqual({
+ state: {
+ layers: {
+ first: {
+ columnOrder: ['col1'],
+ columns: {
+ col1: {
+ label: 'My Op',
+ dataType: 'string',
+ isBucketed: true,
+
+ // Private
+ operationType: 'terms',
+ sourceField: 'op',
+ params: {
+ size: 5,
+ orderBy: { type: 'alphabetical' },
+ orderDirection: 'asc',
+ },
+ },
+ },
+ },
+ },
+ },
+ savedObjectReferences: [
+ { name: 'indexpattern-datasource-current-indexpattern', type: 'index-pattern', id: '1' },
+ { name: 'indexpattern-datasource-layer-first', type: 'index-pattern', id: '1' },
+ ],
+ });
});
});
@@ -237,7 +271,7 @@ describe('IndexPattern Data Source', () => {
});
it('should generate an expression for an aggregated query', async () => {
- const queryPersistedState: IndexPatternPersistedState = {
+ const queryBaseState: IndexPatternBaseState = {
currentIndexPatternId: '1',
layers: {
first: {
@@ -266,7 +300,7 @@ describe('IndexPattern Data Source', () => {
},
};
- const state = stateFromPersistedState(queryPersistedState);
+ const state = enrichBaseState(queryBaseState);
expect(indexPatternDatasource.toExpression(state, 'first')).toMatchInlineSnapshot(`
Object {
@@ -311,7 +345,7 @@ describe('IndexPattern Data Source', () => {
});
it('should put all time fields used in date_histograms to the esaggs timeFields parameter', async () => {
- const queryPersistedState: IndexPatternPersistedState = {
+ const queryBaseState: IndexPatternBaseState = {
currentIndexPatternId: '1',
layers: {
first: {
@@ -350,14 +384,14 @@ describe('IndexPattern Data Source', () => {
},
};
- const state = stateFromPersistedState(queryPersistedState);
+ const state = enrichBaseState(queryBaseState);
const ast = indexPatternDatasource.toExpression(state, 'first') as Ast;
expect(ast.chain[0].arguments.timeFields).toEqual(['timestamp', 'another_datefield']);
});
it('should not put date fields used outside date_histograms to the esaggs timeFields parameter', async () => {
- const queryPersistedState: IndexPatternPersistedState = {
+ const queryBaseState: IndexPatternBaseState = {
currentIndexPatternId: '1',
layers: {
first: {
@@ -386,7 +420,7 @@ describe('IndexPattern Data Source', () => {
},
};
- const state = stateFromPersistedState(queryPersistedState);
+ const state = enrichBaseState(queryBaseState);
const ast = indexPatternDatasource.toExpression(state, 'first') as Ast;
expect(ast.chain[0].arguments.timeFields).toEqual(['timestamp']);
@@ -489,55 +523,14 @@ describe('IndexPattern Data Source', () => {
});
});
- describe('#getMetadata', () => {
- it('should return the title of the index patterns', () => {
- expect(
- indexPatternDatasource.getMetaData({
- indexPatternRefs: [],
- existingFields: {},
- isFirstExistenceFetch: false,
- indexPatterns: expectedIndexPatterns,
- layers: {
- first: {
- indexPatternId: '1',
- columnOrder: [],
- columns: {},
- },
- second: {
- indexPatternId: '2',
- columnOrder: [],
- columns: {},
- },
- },
- currentIndexPatternId: '1',
- })
- ).toEqual({
- filterableIndexPatterns: [
- {
- id: '1',
- title: 'my-fake-index-pattern',
- },
- {
- id: '2',
- title: 'my-fake-restricted-pattern',
- },
- ],
- });
- });
- });
-
describe('#getPublicAPI', () => {
let publicAPI: DatasourcePublicAPI;
beforeEach(async () => {
- const initialState = stateFromPersistedState(persistedState);
+ const initialState = enrichBaseState(baseState);
publicAPI = indexPatternDatasource.getPublicAPI({
state: initialState,
layerId: 'first',
- dateRange: {
- fromDate: 'now-30d',
- toDate: 'now',
- },
});
});
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx
index 2fb8d7fe0e553..e2ca933504849 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx
@@ -8,7 +8,7 @@ import _ from 'lodash';
import React from 'react';
import { render } from 'react-dom';
import { I18nProvider } from '@kbn/i18n/react';
-import { CoreStart } from 'kibana/public';
+import { CoreStart, SavedObjectReference } from 'kibana/public';
import { i18n } from '@kbn/i18n';
import { IStorageWrapper } from 'src/plugins/kibana_utils/public';
import {
@@ -19,7 +19,12 @@ import {
DatasourceLayerPanelProps,
PublicAPIProps,
} from '../types';
-import { loadInitialState, changeIndexPattern, changeLayerIndexPattern } from './loader';
+import {
+ loadInitialState,
+ changeIndexPattern,
+ changeLayerIndexPattern,
+ extractReferences,
+} from './loader';
import { toExpression } from './to_expression';
import {
IndexPatternDimensionTrigger,
@@ -125,9 +130,13 @@ export function getIndexPatternDatasource({
const indexPatternDatasource: Datasource = {
id: 'indexpattern',
- async initialize(state?: IndexPatternPersistedState) {
+ async initialize(
+ persistedState?: IndexPatternPersistedState,
+ references?: SavedObjectReference[]
+ ) {
return loadInitialState({
- state,
+ persistedState,
+ references,
savedObjectsClient: await savedObjectsClient,
defaultIndexPatternId: core.uiSettings.get('defaultIndex'),
storage,
@@ -135,8 +144,8 @@ export function getIndexPatternDatasource({
});
},
- getPersistableState({ currentIndexPatternId, layers }: IndexPatternPrivateState) {
- return { currentIndexPatternId, layers };
+ getPersistableState(state: IndexPatternPrivateState) {
+ return extractReferences(state);
},
insertLayer(state: IndexPatternPrivateState, newLayerId: string) {
@@ -183,19 +192,6 @@ export function getIndexPatternDatasource({
toExpression,
- getMetaData(state: IndexPatternPrivateState) {
- return {
- filterableIndexPatterns: _.uniq(
- Object.values(state.layers)
- .map((layer) => layer.indexPatternId)
- .map((indexPatternId) => ({
- id: indexPatternId,
- title: state.indexPatterns[indexPatternId].title,
- }))
- ),
- };
- },
-
renderDataPanel(
domElement: Element,
props: DatasourceDataPanelProps
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts
index cfabcb4edcef7..d80bf779a5d17 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts
@@ -12,6 +12,8 @@ import {
changeIndexPattern,
changeLayerIndexPattern,
syncExistingFields,
+ extractReferences,
+ injectReferences,
} from './loader';
import { IndexPatternsContract } from '../../../../../src/plugins/data/public';
import {
@@ -378,10 +380,8 @@ describe('loader', () => {
it('should initialize from saved state', async () => {
const savedState: IndexPatternPersistedState = {
- currentIndexPatternId: '2',
layers: {
layerb: {
- indexPatternId: '2',
columnOrder: ['col1', 'col2'],
columns: {
col1: {
@@ -407,7 +407,12 @@ describe('loader', () => {
};
const storage = createMockStorage({ indexPatternId: '1' });
const state = await loadInitialState({
- state: savedState,
+ persistedState: savedState,
+ references: [
+ { name: 'indexpattern-datasource-current-indexpattern', id: '2', type: 'index-pattern' },
+ { name: 'indexpattern-datasource-layer-layerb', id: '2', type: 'index-pattern' },
+ { name: 'another-reference', id: 'c', type: 'index-pattern' },
+ ],
savedObjectsClient: mockClient(),
indexPatternsService: mockIndexPatternsService(),
storage,
@@ -422,7 +427,7 @@ describe('loader', () => {
indexPatterns: {
'2': sampleIndexPatterns['2'],
},
- layers: savedState.layers,
+ layers: { layerb: { ...savedState.layers.layerb, indexPatternId: '2' } },
});
expect(storage.set).toHaveBeenCalledWith('lens-settings', {
@@ -431,6 +436,79 @@ describe('loader', () => {
});
});
+ describe('saved object references', () => {
+ const state: IndexPatternPrivateState = {
+ currentIndexPatternId: 'b',
+ indexPatternRefs: [],
+ indexPatterns: {},
+ existingFields: {},
+ layers: {
+ a: {
+ indexPatternId: 'id-index-pattern-a',
+ columnOrder: ['col1'],
+ columns: {
+ col1: {
+ dataType: 'number',
+ isBucketed: false,
+ label: '',
+ operationType: 'avg',
+ sourceField: 'myfield',
+ },
+ },
+ },
+ b: {
+ indexPatternId: 'id-index-pattern-b',
+ columnOrder: ['col2'],
+ columns: {
+ col2: {
+ dataType: 'number',
+ isBucketed: false,
+ label: '',
+ operationType: 'avg',
+ sourceField: 'myfield2',
+ },
+ },
+ },
+ },
+ isFirstExistenceFetch: false,
+ };
+
+ it('should create a reference for each layer and for current index pattern', () => {
+ const { savedObjectReferences } = extractReferences(state);
+ expect(savedObjectReferences).toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "id": "b",
+ "name": "indexpattern-datasource-current-indexpattern",
+ "type": "index-pattern",
+ },
+ Object {
+ "id": "id-index-pattern-a",
+ "name": "indexpattern-datasource-layer-a",
+ "type": "index-pattern",
+ },
+ Object {
+ "id": "id-index-pattern-b",
+ "name": "indexpattern-datasource-layer-b",
+ "type": "index-pattern",
+ },
+ ]
+ `);
+ });
+
+ it('should restore layers', () => {
+ const { savedObjectReferences, state: persistedState } = extractReferences(state);
+ expect(injectReferences(persistedState, savedObjectReferences).layers).toEqual(state.layers);
+ });
+
+ it('should restore current index pattern', () => {
+ const { savedObjectReferences, state: persistedState } = extractReferences(state);
+ expect(injectReferences(persistedState, savedObjectReferences).currentIndexPatternId).toEqual(
+ state.currentIndexPatternId
+ );
+ });
+ });
+
describe('changeIndexPattern', () => {
it('loads the index pattern and then sets it as current', async () => {
const setState = jest.fn();
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts
index 9c4a19e58a052..24906790a9fc9 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts
@@ -6,7 +6,7 @@
import _ from 'lodash';
import { IStorageWrapper } from 'src/plugins/kibana_utils/public';
-import { SavedObjectsClientContract, HttpSetup } from 'kibana/public';
+import { SavedObjectsClientContract, HttpSetup, SavedObjectReference } from 'kibana/public';
import { StateSetter } from '../types';
import {
IndexPattern,
@@ -14,6 +14,7 @@ import {
IndexPatternPersistedState,
IndexPatternPrivateState,
IndexPatternField,
+ IndexPatternLayer,
} from './types';
import { updateLayerIndexPattern } from './state_helpers';
import { DateRange, ExistingFields } from '../../common/types';
@@ -115,14 +116,58 @@ const setLastUsedIndexPatternId = (storage: IStorageWrapper, value: string) => {
writeToStorage(storage, 'indexPatternId', value);
};
+const CURRENT_PATTERN_REFERENCE_NAME = 'indexpattern-datasource-current-indexpattern';
+function getLayerReferenceName(layerId: string) {
+ return `indexpattern-datasource-layer-${layerId}`;
+}
+
+export function extractReferences({ currentIndexPatternId, layers }: IndexPatternPrivateState) {
+ const savedObjectReferences: SavedObjectReference[] = [];
+ savedObjectReferences.push({
+ type: 'index-pattern',
+ id: currentIndexPatternId,
+ name: CURRENT_PATTERN_REFERENCE_NAME,
+ });
+ const persistableLayers: Record> = {};
+ Object.entries(layers).forEach(([layerId, { indexPatternId, ...persistableLayer }]) => {
+ savedObjectReferences.push({
+ type: 'index-pattern',
+ id: indexPatternId,
+ name: getLayerReferenceName(layerId),
+ });
+ persistableLayers[layerId] = persistableLayer;
+ });
+ return { savedObjectReferences, state: { layers: persistableLayers } };
+}
+
+export function injectReferences(
+ state: IndexPatternPersistedState,
+ references: SavedObjectReference[]
+) {
+ const layers: Record = {};
+ Object.entries(state.layers).forEach(([layerId, persistedLayer]) => {
+ layers[layerId] = {
+ ...persistedLayer,
+ indexPatternId: references.find(({ name }) => name === getLayerReferenceName(layerId))!.id,
+ };
+ });
+ return {
+ currentIndexPatternId: references.find(({ name }) => name === CURRENT_PATTERN_REFERENCE_NAME)!
+ .id,
+ layers,
+ };
+}
+
export async function loadInitialState({
- state,
+ persistedState,
+ references,
savedObjectsClient,
defaultIndexPatternId,
storage,
indexPatternsService,
}: {
- state?: IndexPatternPersistedState;
+ persistedState?: IndexPatternPersistedState;
+ references?: SavedObjectReference[];
savedObjectsClient: SavedObjectsClient;
defaultIndexPatternId?: string;
storage: IStorageWrapper;
@@ -131,6 +176,9 @@ export async function loadInitialState({
const indexPatternRefs = await loadIndexPatternRefs(savedObjectsClient);
const lastUsedIndexPatternId = getLastUsedIndexPatternId(storage, indexPatternRefs);
+ const state =
+ persistedState && references ? injectReferences(persistedState, references) : undefined;
+
const requiredPatterns = _.uniq(
state
? Object.values(state.layers)
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/types.ts b/x-pack/plugins/lens/public/indexpattern_datasource/types.ts
index 8d0e82b176aa9..95cc47e68f8a1 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/types.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/types.ts
@@ -40,11 +40,12 @@ export interface IndexPatternLayer {
}
export interface IndexPatternPersistedState {
- currentIndexPatternId: string;
- layers: Record;
+ layers: Record>;
}
-export type IndexPatternPrivateState = IndexPatternPersistedState & {
+export interface IndexPatternPrivateState {
+ currentIndexPatternId: string;
+ layers: Record;
indexPatternRefs: IndexPatternRef[];
indexPatterns: Record;
@@ -54,7 +55,7 @@ export type IndexPatternPrivateState = IndexPatternPersistedState & {
existingFields: Record>;
isFirstExistenceFetch: boolean;
existenceFetchFailed?: boolean;
-};
+}
export interface IndexPatternRef {
id: string;
diff --git a/x-pack/plugins/lens/public/metric_visualization/metric_visualization.test.ts b/x-pack/plugins/lens/public/metric_visualization/metric_visualization.test.ts
index 62f47a21c85b0..f3c9a725ee2e2 100644
--- a/x-pack/plugins/lens/public/metric_visualization/metric_visualization.test.ts
+++ b/x-pack/plugins/lens/public/metric_visualization/metric_visualization.test.ts
@@ -66,12 +66,6 @@ describe('metric_visualization', () => {
});
});
- describe('#getPersistableState', () => {
- it('persists the state as given', () => {
- expect(metricVisualization.getPersistableState(exampleState())).toEqual(exampleState());
- });
- });
-
describe('#getConfiguration', () => {
it('can add a metric when there is no accessor', () => {
expect(
@@ -168,7 +162,8 @@ describe('metric_visualization', () => {
datasourceLayers: { l1: datasource },
};
- expect(metricVisualization.toExpression(exampleState(), frame)).toMatchInlineSnapshot(`
+ expect(metricVisualization.toExpression(exampleState(), frame.datasourceLayers))
+ .toMatchInlineSnapshot(`
Object {
"chain": Array [
Object {
diff --git a/x-pack/plugins/lens/public/metric_visualization/metric_visualization.tsx b/x-pack/plugins/lens/public/metric_visualization/metric_visualization.tsx
index e565d2fa8b293..5f1ce5334dd36 100644
--- a/x-pack/plugins/lens/public/metric_visualization/metric_visualization.tsx
+++ b/x-pack/plugins/lens/public/metric_visualization/metric_visualization.tsx
@@ -7,20 +7,20 @@
import { i18n } from '@kbn/i18n';
import { Ast } from '@kbn/interpreter/target/common';
import { getSuggestions } from './metric_suggestions';
-import { Visualization, FramePublicAPI, OperationMetadata } from '../types';
-import { State, PersistableState } from './types';
+import { Visualization, OperationMetadata, DatasourcePublicAPI } from '../types';
+import { State } from './types';
import chartMetricSVG from '../assets/chart_metric.svg';
const toExpression = (
state: State,
- frame: FramePublicAPI,
+ datasourceLayers: Record,
mode: 'reduced' | 'full' = 'full'
): Ast | null => {
if (!state.accessor) {
return null;
}
- const [datasource] = Object.values(frame.datasourceLayers);
+ const [datasource] = Object.values(datasourceLayers);
const operation = datasource && datasource.getOperationForColumnId(state.accessor);
return {
@@ -39,7 +39,7 @@ const toExpression = (
};
};
-export const metricVisualization: Visualization = {
+export const metricVisualization: Visualization = {
id: 'lnsMetric',
visualizationTypes: [
@@ -88,8 +88,6 @@ export const metricVisualization: Visualization = {
);
},
- getPersistableState: (state) => state,
-
getConfiguration(props) {
return {
groups: [
@@ -106,8 +104,8 @@ export const metricVisualization: Visualization = {
},
toExpression,
- toPreviewExpression: (state: State, frame: FramePublicAPI) =>
- toExpression(state, frame, 'reduced'),
+ toPreviewExpression: (state, datasourceLayers) =>
+ toExpression(state, datasourceLayers, 'reduced'),
setDimension({ prevState, columnId }) {
return { ...prevState, accessor: columnId };
diff --git a/x-pack/plugins/lens/public/metric_visualization/types.ts b/x-pack/plugins/lens/public/metric_visualization/types.ts
index 53fc103934255..86a781716b345 100644
--- a/x-pack/plugins/lens/public/metric_visualization/types.ts
+++ b/x-pack/plugins/lens/public/metric_visualization/types.ts
@@ -13,5 +13,3 @@ export interface MetricConfig extends State {
title: string;
mode: 'reduced' | 'full';
}
-
-export type PersistableState = State;
diff --git a/x-pack/plugins/lens/public/persistence/filter_references.test.ts b/x-pack/plugins/lens/public/persistence/filter_references.test.ts
new file mode 100644
index 0000000000000..23c0cd1d11f1b
--- /dev/null
+++ b/x-pack/plugins/lens/public/persistence/filter_references.test.ts
@@ -0,0 +1,99 @@
+/*
+ * 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 { Filter } from 'src/plugins/data/public';
+import { extractFilterReferences, injectFilterReferences } from './filter_references';
+import { FilterStateStore } from 'src/plugins/data/common';
+
+describe('filter saved object references', () => {
+ const filters: Filter[] = [
+ {
+ $state: { store: FilterStateStore.APP_STATE },
+ meta: {
+ alias: null,
+ disabled: false,
+ index: '90943e30-9a47-11e8-b64d-95841ca0b247',
+ key: 'geo.src',
+ negate: true,
+ params: { query: 'CN' },
+ type: 'phrase',
+ },
+ query: { match_phrase: { 'geo.src': 'CN' } },
+ },
+ {
+ $state: { store: FilterStateStore.APP_STATE },
+ meta: {
+ alias: null,
+ disabled: false,
+ index: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
+ key: 'geoip.country_iso_code',
+ negate: true,
+ params: { query: 'US' },
+ type: 'phrase',
+ },
+ query: { match_phrase: { 'geoip.country_iso_code': 'US' } },
+ },
+ ];
+
+ it('should create two index-pattern references', () => {
+ const { references } = extractFilterReferences(filters);
+ expect(references).toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "id": "90943e30-9a47-11e8-b64d-95841ca0b247",
+ "name": "filter-index-pattern-0",
+ "type": "index-pattern",
+ },
+ Object {
+ "id": "ff959d40-b880-11e8-a6d9-e546fe2bba5f",
+ "name": "filter-index-pattern-1",
+ "type": "index-pattern",
+ },
+ ]
+ `);
+ });
+
+ it('should restore the same filter after extracting and injecting', () => {
+ const { persistableFilters, references } = extractFilterReferences(filters);
+ expect(injectFilterReferences(persistableFilters, references)).toEqual(filters);
+ });
+
+ it('should ignore other references', () => {
+ const { persistableFilters, references } = extractFilterReferences(filters);
+ expect(
+ injectFilterReferences(persistableFilters, [
+ { type: 'index-pattern', id: '1234', name: 'some other index pattern' },
+ ...references,
+ ])
+ ).toEqual(filters);
+ });
+
+ it('should inject other ids if references change', () => {
+ const { persistableFilters, references } = extractFilterReferences(filters);
+
+ expect(
+ injectFilterReferences(
+ persistableFilters,
+ references.map((reference, index) => ({ ...reference, id: `overwritten-id-${index}` }))
+ )
+ ).toEqual([
+ {
+ ...filters[0],
+ meta: {
+ ...filters[0].meta,
+ index: 'overwritten-id-0',
+ },
+ },
+ {
+ ...filters[1],
+ meta: {
+ ...filters[1].meta,
+ index: 'overwritten-id-1',
+ },
+ },
+ ]);
+ });
+});
diff --git a/x-pack/plugins/lens/public/persistence/filter_references.ts b/x-pack/plugins/lens/public/persistence/filter_references.ts
new file mode 100644
index 0000000000000..47564e510ce9c
--- /dev/null
+++ b/x-pack/plugins/lens/public/persistence/filter_references.ts
@@ -0,0 +1,56 @@
+/*
+ * 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 { Filter } from 'src/plugins/data/public';
+import { SavedObjectReference } from 'kibana/public';
+import { PersistableFilter } from '../../common';
+
+export function extractFilterReferences(
+ filters: Filter[]
+): { persistableFilters: PersistableFilter[]; references: SavedObjectReference[] } {
+ const references: SavedObjectReference[] = [];
+ const persistableFilters = filters.map((filterRow, i) => {
+ if (!filterRow.meta || !filterRow.meta.index) {
+ return filterRow;
+ }
+ const refName = `filter-index-pattern-${i}`;
+ references.push({
+ name: refName,
+ type: 'index-pattern',
+ id: filterRow.meta.index,
+ });
+ return {
+ ...filterRow,
+ meta: {
+ ...filterRow.meta,
+ indexRefName: refName,
+ index: undefined,
+ },
+ };
+ });
+
+ return { persistableFilters, references };
+}
+
+export function injectFilterReferences(
+ filters: PersistableFilter[],
+ references: SavedObjectReference[]
+) {
+ return filters.map((filterRow) => {
+ if (!filterRow.meta || !filterRow.meta.indexRefName) {
+ return filterRow as Filter;
+ }
+ const { indexRefName, ...metaRest } = filterRow.meta;
+ const reference = references.find((ref) => ref.name === indexRefName);
+ if (!reference) {
+ throw new Error(`Could not find reference for ${indexRefName}`);
+ }
+ return {
+ ...filterRow,
+ meta: { ...metaRest, index: reference.id },
+ };
+ });
+}
diff --git a/x-pack/plugins/lens/public/persistence/index.ts b/x-pack/plugins/lens/public/persistence/index.ts
index 1f823ff75c8c6..464bd46790422 100644
--- a/x-pack/plugins/lens/public/persistence/index.ts
+++ b/x-pack/plugins/lens/public/persistence/index.ts
@@ -5,3 +5,4 @@
*/
export * from './saved_object_store';
+export * from './filter_references';
diff --git a/x-pack/plugins/lens/public/persistence/saved_object_store.test.ts b/x-pack/plugins/lens/public/persistence/saved_object_store.test.ts
index f8f8d889233a7..ba7c0ee6ae786 100644
--- a/x-pack/plugins/lens/public/persistence/saved_object_store.test.ts
+++ b/x-pack/plugins/lens/public/persistence/saved_object_store.test.ts
@@ -30,11 +30,8 @@ describe('LensStore', () => {
title: 'Hello',
description: 'My doc',
visualizationType: 'bar',
- expression: '',
+ references: [],
state: {
- datasourceMetaData: {
- filterableIndexPatterns: [],
- },
datasourceStates: {
indexpattern: { type: 'index_pattern', indexPattern: '.kibana_test' },
},
@@ -49,11 +46,8 @@ describe('LensStore', () => {
title: 'Hello',
description: 'My doc',
visualizationType: 'bar',
- expression: '',
+ references: [],
state: {
- datasourceMetaData: {
- filterableIndexPatterns: [],
- },
datasourceStates: {
indexpattern: { type: 'index_pattern', indexPattern: '.kibana_test' },
},
@@ -64,21 +58,25 @@ describe('LensStore', () => {
});
expect(client.create).toHaveBeenCalledTimes(1);
- expect(client.create).toHaveBeenCalledWith('lens', {
- title: 'Hello',
- description: 'My doc',
- visualizationType: 'bar',
- expression: '',
- state: {
- datasourceMetaData: { filterableIndexPatterns: [] },
- datasourceStates: {
- indexpattern: { type: 'index_pattern', indexPattern: '.kibana_test' },
+ expect(client.create).toHaveBeenCalledWith(
+ 'lens',
+ {
+ title: 'Hello',
+ description: 'My doc',
+ visualizationType: 'bar',
+ state: {
+ datasourceStates: {
+ indexpattern: { type: 'index_pattern', indexPattern: '.kibana_test' },
+ },
+ visualization: { x: 'foo', y: 'baz' },
+ query: { query: '', language: 'lucene' },
+ filters: [],
},
- visualization: { x: 'foo', y: 'baz' },
- query: { query: '', language: 'lucene' },
- filters: [],
},
- });
+ {
+ references: [],
+ }
+ );
});
test('updates and returns a visualization document', async () => {
@@ -87,9 +85,8 @@ describe('LensStore', () => {
id: 'Gandalf',
title: 'Even the very wise cannot see all ends.',
visualizationType: 'line',
- expression: '',
+ references: [],
state: {
- datasourceMetaData: { filterableIndexPatterns: [] },
datasourceStates: { indexpattern: { type: 'index_pattern', indexPattern: 'lotr' } },
visualization: { gear: ['staff', 'pointy hat'] },
query: { query: '', language: 'lucene' },
@@ -101,9 +98,8 @@ describe('LensStore', () => {
id: 'Gandalf',
title: 'Even the very wise cannot see all ends.',
visualizationType: 'line',
- expression: '',
+ references: [],
state: {
- datasourceMetaData: { filterableIndexPatterns: [] },
datasourceStates: { indexpattern: { type: 'index_pattern', indexPattern: 'lotr' } },
visualization: { gear: ['staff', 'pointy hat'] },
query: { query: '', language: 'lucene' },
@@ -116,22 +112,21 @@ describe('LensStore', () => {
{
type: 'lens',
id: 'Gandalf',
+ references: [],
attributes: {
title: null,
visualizationType: null,
- expression: null,
state: null,
},
},
{
type: 'lens',
id: 'Gandalf',
+ references: [],
attributes: {
title: 'Even the very wise cannot see all ends.',
visualizationType: 'line',
- expression: '',
state: {
- datasourceMetaData: { filterableIndexPatterns: [] },
datasourceStates: { indexpattern: { type: 'index_pattern', indexPattern: 'lotr' } },
visualization: { gear: ['staff', 'pointy hat'] },
query: { query: '', language: 'lucene' },
diff --git a/x-pack/plugins/lens/public/persistence/saved_object_store.ts b/x-pack/plugins/lens/public/persistence/saved_object_store.ts
index 59ead53956a8d..e4609213ec792 100644
--- a/x-pack/plugins/lens/public/persistence/saved_object_store.ts
+++ b/x-pack/plugins/lens/public/persistence/saved_object_store.ts
@@ -4,8 +4,13 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { SavedObjectAttributes, SavedObjectsClientContract } from 'kibana/public';
-import { Query, Filter } from '../../../../../src/plugins/data/public';
+import {
+ SavedObjectAttributes,
+ SavedObjectsClientContract,
+ SavedObjectReference,
+} from 'kibana/public';
+import { Query } from '../../../../../src/plugins/data/public';
+import { PersistableFilter } from '../../common';
export interface Document {
id?: string;
@@ -13,16 +18,13 @@ export interface Document {
visualizationType: string | null;
title: string;
description?: string;
- expression: string | null;
state: {
- datasourceMetaData: {
- filterableIndexPatterns: Array<{ id: string; title: string }>;
- };
datasourceStates: Record;
visualization: unknown;
query: Query;
- filters: Filter[];
+ filters: PersistableFilter[];
};
+ references: SavedObjectReference[];
}
export const DOC_TYPE = 'lens';
@@ -45,14 +47,16 @@ export class SavedObjectIndexStore implements SavedObjectStore {
}
async save(vis: Document) {
- const { id, type, ...rest } = vis;
+ const { id, type, references, ...rest } = vis;
// TODO: SavedObjectAttributes should support this kind of object,
// remove this workaround when SavedObjectAttributes is updated.
const attributes = (rest as unknown) as SavedObjectAttributes;
const result = await (id
- ? this.safeUpdate(id, attributes)
- : this.client.create(DOC_TYPE, attributes));
+ ? this.safeUpdate(id, attributes, references)
+ : this.client.create(DOC_TYPE, attributes, {
+ references,
+ }));
return { ...vis, id: result.id };
}
@@ -63,21 +67,25 @@ export class SavedObjectIndexStore implements SavedObjectStore {
// deleted subtrees make it back into the object after a load.
// This function fixes this by doing two updates - one to empty out the document setting
// every key to null, and a second one to load the new content.
- private async safeUpdate(id: string, attributes: SavedObjectAttributes) {
+ private async safeUpdate(
+ id: string,
+ attributes: SavedObjectAttributes,
+ references: SavedObjectReference[]
+ ) {
const resetAttributes: SavedObjectAttributes = {};
Object.keys(attributes).forEach((key) => {
resetAttributes[key] = null;
});
return (
await this.client.bulkUpdate([
- { type: DOC_TYPE, id, attributes: resetAttributes },
- { type: DOC_TYPE, id, attributes },
+ { type: DOC_TYPE, id, attributes: resetAttributes, references },
+ { type: DOC_TYPE, id, attributes, references },
])
).savedObjects[1];
}
async load(id: string): Promise {
- const { type, attributes, error } = await this.client.get(DOC_TYPE, id);
+ const { type, attributes, references, error } = await this.client.get(DOC_TYPE, id);
if (error) {
throw error;
@@ -85,6 +93,7 @@ export class SavedObjectIndexStore implements SavedObjectStore {
return {
...(attributes as SavedObjectAttributes),
+ references,
id,
type,
} as Document;
diff --git a/x-pack/plugins/lens/public/pie_visualization/pie_visualization.tsx b/x-pack/plugins/lens/public/pie_visualization/pie_visualization.tsx
index 5a68516db6aa3..855bacd4f794c 100644
--- a/x-pack/plugins/lens/public/pie_visualization/pie_visualization.tsx
+++ b/x-pack/plugins/lens/public/pie_visualization/pie_visualization.tsx
@@ -31,7 +31,7 @@ const bucketedOperations = (op: OperationMetadata) => op.isBucketed;
const numberMetricOperations = (op: OperationMetadata) =>
!op.isBucketed && op.dataType === 'number';
-export const pieVisualization: Visualization = {
+export const pieVisualization: Visualization = {
id: 'lnsPie',
visualizationTypes: [
@@ -91,8 +91,6 @@ export const pieVisualization: Visualization state,
-
getSuggestions: suggestions,
getConfiguration({ state, frame, layerId }) {
diff --git a/x-pack/plugins/lens/public/pie_visualization/to_expression.ts b/x-pack/plugins/lens/public/pie_visualization/to_expression.ts
index fbc47e8bfb00f..f36b9efb930a9 100644
--- a/x-pack/plugins/lens/public/pie_visualization/to_expression.ts
+++ b/x-pack/plugins/lens/public/pie_visualization/to_expression.ts
@@ -5,21 +5,24 @@
*/
import { Ast } from '@kbn/interpreter/common';
-import { FramePublicAPI, Operation } from '../types';
+import { Operation, DatasourcePublicAPI } from '../types';
import { DEFAULT_PERCENT_DECIMALS } from './constants';
import { PieVisualizationState } from './types';
-export function toExpression(state: PieVisualizationState, frame: FramePublicAPI) {
- return expressionHelper(state, frame, false);
+export function toExpression(
+ state: PieVisualizationState,
+ datasourceLayers: Record
+) {
+ return expressionHelper(state, datasourceLayers, false);
}
function expressionHelper(
state: PieVisualizationState,
- frame: FramePublicAPI,
+ datasourceLayers: Record,
isPreview: boolean
): Ast | null {
const layer = state.layers[0];
- const datasource = frame.datasourceLayers[layer.layerId];
+ const datasource = datasourceLayers[layer.layerId];
const operations = layer.groups
.map((columnId) => ({ columnId, operation: datasource.getOperationForColumnId(columnId) }))
.filter((o): o is { columnId: string; operation: Operation } => !!o.operation);
@@ -50,6 +53,9 @@ function expressionHelper(
};
}
-export function toPreviewExpression(state: PieVisualizationState, frame: FramePublicAPI) {
- return expressionHelper(state, frame, true);
+export function toPreviewExpression(
+ state: PieVisualizationState,
+ datasourceLayers: Record
+) {
+ return expressionHelper(state, datasourceLayers, true);
}
diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts
index c7bda65cd1327..20f2ce6c56774 100644
--- a/x-pack/plugins/lens/public/types.ts
+++ b/x-pack/plugins/lens/public/types.ts
@@ -7,6 +7,7 @@
import { Ast } from '@kbn/interpreter/common';
import { IconType } from '@elastic/eui/src/components/icon/icon';
import { CoreSetup } from 'kibana/public';
+import { SavedObjectReference } from 'kibana/public';
import {
ExpressionRendererEvent,
IInterpreterRenderHandlers,
@@ -30,7 +31,6 @@ export type FormatFactory = (mapping?: SerializedFieldFormat) => IFieldFormat;
export interface PublicAPIProps {
state: T;
layerId: string;
- dateRange: DateRange;
}
export interface EditorFrameProps {
@@ -44,8 +44,9 @@ export interface EditorFrameProps {
// Frame loader (app or embeddable) is expected to call this when it loads and updates
// This should be replaced with a top-down state
onChange: (newState: {
- filterableIndexPatterns: DatasourceMetaData['filterableIndexPatterns'];
+ filterableIndexPatterns: string[];
doc: Document;
+ isSaveable: boolean;
}) => void;
showNoDataPopover: () => void;
}
@@ -57,9 +58,7 @@ export interface EditorFrameInstance {
export interface EditorFrameSetup {
// generic type on the API functions to pull the "unknown vs. specific type" error into the implementation
registerDatasource: (datasource: Datasource | Promise>) => void;
- registerVisualization: (
- visualization: Visualization | Promise>
- ) => void;
+ registerVisualization: (visualization: Visualization | Promise>) => void;
}
export interface EditorFrameStart {
@@ -131,10 +130,6 @@ export interface DatasourceSuggestion {
keptLayerIds: string[];
}
-export interface DatasourceMetaData {
- filterableIndexPatterns: Array<{ id: string; title: string }>;
-}
-
export type StateSetter = (newState: T | ((prevState: T) => T)) => void;
/**
@@ -146,10 +141,10 @@ export interface Datasource {
// For initializing, either from an empty state or from persisted state
// Because this will be called at runtime, state might have a type of `any` and
// datasources should validate their arguments
- initialize: (state?: P) => Promise;
+ initialize: (state?: P, savedObjectReferences?: SavedObjectReference[]) => Promise;
// Given the current state, which parts should be saved?
- getPersistableState: (state: T) => P;
+ getPersistableState: (state: T) => { state: P; savedObjectReferences: SavedObjectReference[] };
insertLayer: (state: T, newLayerId: string) => T;
removeLayer: (state: T, layerId: string) => T;
@@ -166,8 +161,6 @@ export interface Datasource {
toExpression: (state: T, layerId: string) => Ast | string | null;
- getMetaData: (state: T) => DatasourceMetaData;
-
getDatasourceSuggestionsForField: (state: T, field: unknown) => Array>;
getDatasourceSuggestionsFromCurrentState: (state: T) => Array>;
@@ -408,7 +401,7 @@ export interface VisualizationType {
label: string;
}
-export interface Visualization {
+export interface Visualization {
/** Plugin ID, such as "lnsXY" */
id: string;
@@ -418,11 +411,7 @@ export interface Visualization {
* - Loadingn from a saved visualization
* - When using suggestions, the suggested state is passed in
*/
- initialize: (frame: FramePublicAPI, state?: P) => T;
- /**
- * Can remove any state that should not be persisted to saved object, such as UI state
- */
- getPersistableState: (state: T) => P;
+ initialize: (frame: FramePublicAPI, state?: T) => T;
/**
* Visualizations must provide at least one type for the chart switcher,
@@ -504,12 +493,18 @@ export interface Visualization {
*/
getSuggestions: (context: SuggestionRequest) => Array>;
- toExpression: (state: T, frame: FramePublicAPI) => Ast | string | null;
+ toExpression: (
+ state: T,
+ datasourceLayers: Record
+ ) => Ast | string | null;
/**
* Expression to render a preview version of the chart in very constrained space.
* If there is no expression provided, the preview icon is used.
*/
- toPreviewExpression?: (state: T, frame: FramePublicAPI) => Ast | string | null;
+ toPreviewExpression?: (
+ state: T,
+ datasourceLayers: Record
+ ) => Ast | string | null;
}
export interface LensFilterEvent {
diff --git a/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts b/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts
index 876d1141740e1..f579085646f6f 100644
--- a/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts
+++ b/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts
@@ -53,7 +53,7 @@ describe('#toExpression', () => {
},
],
},
- frame
+ frame.datasourceLayers
)
).toMatchSnapshot();
});
@@ -74,7 +74,7 @@ describe('#toExpression', () => {
},
],
},
- frame
+ frame.datasourceLayers
) as Ast).chain[0].arguments.fittingFunction[0]
).toEqual('None');
});
@@ -94,7 +94,7 @@ describe('#toExpression', () => {
},
],
},
- frame
+ frame.datasourceLayers
) as Ast;
expect(expression.chain[0].arguments.showXAxisTitle[0]).toBe(true);
expect(expression.chain[0].arguments.showYAxisTitle[0]).toBe(true);
@@ -116,7 +116,7 @@ describe('#toExpression', () => {
},
],
},
- frame
+ frame.datasourceLayers
)
).toBeNull();
});
@@ -137,7 +137,7 @@ describe('#toExpression', () => {
},
],
},
- frame
+ frame.datasourceLayers
)
).toBeNull();
});
@@ -157,7 +157,7 @@ describe('#toExpression', () => {
},
],
},
- frame
+ frame.datasourceLayers
)! as Ast;
expect(mockDatasource.publicAPIMock.getOperationForColumnId).toHaveBeenCalledWith('b');
@@ -191,7 +191,7 @@ describe('#toExpression', () => {
},
],
},
- frame
+ frame.datasourceLayers
) as Ast;
expect(
(expression.chain[0].arguments.tickLabelsVisibilitySettings[0] as Ast).chain[0].arguments
@@ -216,7 +216,7 @@ describe('#toExpression', () => {
},
],
},
- frame
+ frame.datasourceLayers
) as Ast;
expect(
(expression.chain[0].arguments.gridlinesVisibilitySettings[0] as Ast).chain[0].arguments
diff --git a/x-pack/plugins/lens/public/xy_visualization/to_expression.ts b/x-pack/plugins/lens/public/xy_visualization/to_expression.ts
index 9b9c159af265e..cd32d4f94c3e5 100644
--- a/x-pack/plugins/lens/public/xy_visualization/to_expression.ts
+++ b/x-pack/plugins/lens/public/xy_visualization/to_expression.ts
@@ -7,13 +7,16 @@
import { Ast } from '@kbn/interpreter/common';
import { ScaleType } from '@elastic/charts';
import { State, LayerConfig } from './types';
-import { FramePublicAPI, OperationMetadata } from '../types';
+import { OperationMetadata, DatasourcePublicAPI } from '../types';
interface ValidLayer extends LayerConfig {
xAccessor: NonNullable;
}
-export const toExpression = (state: State, frame: FramePublicAPI): Ast | null => {
+export const toExpression = (
+ state: State,
+ datasourceLayers: Record
+): Ast | null => {
if (!state || !state.layers.length) {
return null;
}
@@ -21,19 +24,20 @@ export const toExpression = (state: State, frame: FramePublicAPI): Ast | null =>
const metadata: Record> = {};
state.layers.forEach((layer) => {
metadata[layer.layerId] = {};
- const datasource = frame.datasourceLayers[layer.layerId];
+ const datasource = datasourceLayers[layer.layerId];
datasource.getTableSpec().forEach((column) => {
- const operation = frame.datasourceLayers[layer.layerId].getOperationForColumnId(
- column.columnId
- );
+ const operation = datasourceLayers[layer.layerId].getOperationForColumnId(column.columnId);
metadata[layer.layerId][column.columnId] = operation;
});
});
- return buildExpression(state, metadata, frame);
+ return buildExpression(state, metadata, datasourceLayers);
};
-export function toPreviewExpression(state: State, frame: FramePublicAPI) {
+export function toPreviewExpression(
+ state: State,
+ datasourceLayers: Record
+) {
return toExpression(
{
...state,
@@ -44,7 +48,7 @@ export function toPreviewExpression(state: State, frame: FramePublicAPI) {
isVisible: false,
},
},
- frame
+ datasourceLayers
);
}
@@ -77,7 +81,7 @@ export function getScaleType(metadata: OperationMetadata | null, defaultScale: S
export const buildExpression = (
state: State,
metadata: Record>,
- frame?: FramePublicAPI
+ datasourceLayers?: Record
): Ast | null => {
const validLayers = state.layers.filter((layer): layer is ValidLayer =>
Boolean(layer.xAccessor && layer.accessors.length)
@@ -149,8 +153,8 @@ export const buildExpression = (
layers: validLayers.map((layer) => {
const columnToLabel: Record = {};
- if (frame) {
- const datasource = frame.datasourceLayers[layer.layerId];
+ if (datasourceLayers) {
+ const datasource = datasourceLayers[layer.layerId];
layer.accessors
.concat(layer.splitAccessor ? [layer.splitAccessor] : [])
.forEach((accessor) => {
@@ -162,8 +166,8 @@ export const buildExpression = (
}
const xAxisOperation =
- frame &&
- frame.datasourceLayers[layer.layerId].getOperationForColumnId(layer.xAccessor);
+ datasourceLayers &&
+ datasourceLayers[layer.layerId].getOperationForColumnId(layer.xAccessor);
const isHistogramDimension = Boolean(
xAxisOperation &&
diff --git a/x-pack/plugins/lens/public/xy_visualization/types.ts b/x-pack/plugins/lens/public/xy_visualization/types.ts
index ab689ceb183be..2739ffe42f13f 100644
--- a/x-pack/plugins/lens/public/xy_visualization/types.ts
+++ b/x-pack/plugins/lens/public/xy_visualization/types.ts
@@ -339,7 +339,6 @@ export interface XYState {
}
export type State = XYState;
-export type PersistableState = XYState;
export const visualizationTypes: VisualizationType[] = [
{
diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_visualization.test.ts b/x-pack/plugins/lens/public/xy_visualization/xy_visualization.test.ts
index 0a8e8bbe0c46f..53f7a23dcae98 100644
--- a/x-pack/plugins/lens/public/xy_visualization/xy_visualization.test.ts
+++ b/x-pack/plugins/lens/public/xy_visualization/xy_visualization.test.ts
@@ -157,12 +157,6 @@ describe('xy_visualization', () => {
});
});
- describe('#getPersistableState', () => {
- it('persists the state as given', () => {
- expect(xyVisualization.getPersistableState(exampleState())).toEqual(exampleState());
- });
- });
-
describe('#removeLayer', () => {
it('removes the specified layer', () => {
const prevState: State = {
diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_visualization.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_visualization.tsx
index f321e0962caa8..8c551c575764e 100644
--- a/x-pack/plugins/lens/public/xy_visualization/xy_visualization.tsx
+++ b/x-pack/plugins/lens/public/xy_visualization/xy_visualization.tsx
@@ -13,7 +13,7 @@ import { i18n } from '@kbn/i18n';
import { getSuggestions } from './xy_suggestions';
import { LayerContextMenu, XyToolbar, DimensionEditor } from './xy_config_panel';
import { Visualization, OperationMetadata, VisualizationType } from '../types';
-import { State, PersistableState, SeriesType, visualizationTypes, LayerConfig } from './types';
+import { State, SeriesType, visualizationTypes, LayerConfig } from './types';
import chartBarStackedSVG from '../assets/chart_bar_stacked.svg';
import chartMixedSVG from '../assets/chart_mixed_xy.svg';
import { isHorizontalChart } from './state_helpers';
@@ -74,7 +74,7 @@ function getDescription(state?: State) {
};
}
-export const xyVisualization: Visualization = {
+export const xyVisualization: Visualization = {
id: 'lnsXY',
visualizationTypes,
@@ -159,8 +159,6 @@ export const xyVisualization: Visualization = {
);
},
- getPersistableState: (state) => state,
-
getConfiguration(props) {
const layer = props.state.layers.find((l) => l.layerId === props.layerId)!;
return {
diff --git a/x-pack/plugins/lens/server/__snapshots__/migrations.test.ts.snap b/x-pack/plugins/lens/server/__snapshots__/migrations.test.ts.snap
new file mode 100644
index 0000000000000..4979438dbd3d0
--- /dev/null
+++ b/x-pack/plugins/lens/server/__snapshots__/migrations.test.ts.snap
@@ -0,0 +1,188 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Lens migrations 7.10.0 references should produce a valid document 1`] = `
+Object {
+ "attributes": Object {
+ "state": Object {
+ "datasourceStates": Object {
+ "indexpattern": Object {
+ "layers": Object {
+ "3b7791e9-326e-40d5-a787-b7594e48d906": Object {
+ "columnOrder": Array [
+ "77d8383e-f66e-471e-ae50-c427feedb5ba",
+ "a5c1b82d-51de-4448-a99d-6391432c3a03",
+ ],
+ "columns": Object {
+ "77d8383e-f66e-471e-ae50-c427feedb5ba": Object {
+ "dataType": "string",
+ "isBucketed": true,
+ "label": "Top values of geoip.country_iso_code",
+ "operationType": "terms",
+ "params": Object {
+ "orderBy": Object {
+ "columnId": "a5c1b82d-51de-4448-a99d-6391432c3a03",
+ "type": "column",
+ },
+ "orderDirection": "desc",
+ "size": 5,
+ },
+ "scale": "ordinal",
+ "sourceField": "geoip.country_iso_code",
+ },
+ "a5c1b82d-51de-4448-a99d-6391432c3a03": Object {
+ "dataType": "number",
+ "isBucketed": false,
+ "label": "Count of records",
+ "operationType": "count",
+ "scale": "ratio",
+ "sourceField": "Records",
+ },
+ },
+ },
+ "9a27f85d-35a9-4246-81b2-48e7ee9b0707": Object {
+ "columnOrder": Array [
+ "96352896-c508-4fca-90d8-66e9ebfce621",
+ "4ce9b4c7-2ebf-4d48-8669-0ea69d973353",
+ ],
+ "columns": Object {
+ "4ce9b4c7-2ebf-4d48-8669-0ea69d973353": Object {
+ "dataType": "number",
+ "isBucketed": false,
+ "label": "Count of records",
+ "operationType": "count",
+ "scale": "ratio",
+ "sourceField": "Records",
+ },
+ "96352896-c508-4fca-90d8-66e9ebfce621": Object {
+ "dataType": "string",
+ "isBucketed": true,
+ "label": "Top values of geo.src",
+ "operationType": "terms",
+ "params": Object {
+ "orderBy": Object {
+ "columnId": "4ce9b4c7-2ebf-4d48-8669-0ea69d973353",
+ "type": "column",
+ },
+ "orderDirection": "desc",
+ "size": 5,
+ },
+ "scale": "ordinal",
+ "sourceField": "geo.src",
+ },
+ },
+ },
+ },
+ },
+ },
+ "filters": Array [
+ Object {
+ "$state": Object {
+ "store": "appState",
+ },
+ "meta": Object {
+ "alias": null,
+ "disabled": false,
+ "index": undefined,
+ "indexRefName": "filter-index-pattern-0",
+ "key": "geo.src",
+ "negate": true,
+ "params": Object {
+ "query": "CN",
+ },
+ "type": "phrase",
+ },
+ "query": Object {
+ "match_phrase": Object {
+ "geo.src": "CN",
+ },
+ },
+ },
+ Object {
+ "$state": Object {
+ "store": "appState",
+ },
+ "meta": Object {
+ "alias": null,
+ "disabled": false,
+ "index": undefined,
+ "indexRefName": "filter-index-pattern-1",
+ "key": "geoip.country_iso_code",
+ "negate": true,
+ "params": Object {
+ "query": "US",
+ },
+ "type": "phrase",
+ },
+ "query": Object {
+ "match_phrase": Object {
+ "geoip.country_iso_code": "US",
+ },
+ },
+ },
+ ],
+ "query": Object {
+ "language": "kuery",
+ "query": "NOT bytes > 5000",
+ },
+ "visualization": Object {
+ "fittingFunction": "None",
+ "layers": Array [
+ Object {
+ "accessors": Array [
+ "4ce9b4c7-2ebf-4d48-8669-0ea69d973353",
+ ],
+ "layerId": "9a27f85d-35a9-4246-81b2-48e7ee9b0707",
+ "position": "top",
+ "seriesType": "bar",
+ "showGridlines": false,
+ "xAccessor": "96352896-c508-4fca-90d8-66e9ebfce621",
+ },
+ Object {
+ "accessors": Array [
+ "a5c1b82d-51de-4448-a99d-6391432c3a03",
+ ],
+ "layerId": "3b7791e9-326e-40d5-a787-b7594e48d906",
+ "seriesType": "bar",
+ "xAccessor": "77d8383e-f66e-471e-ae50-c427feedb5ba",
+ },
+ ],
+ "legend": Object {
+ "isVisible": true,
+ "position": "right",
+ },
+ "preferredSeriesType": "bar",
+ },
+ },
+ "title": "mylens",
+ "visualizationType": "lnsXY",
+ },
+ "references": Array [
+ Object {
+ "id": "ff959d40-b880-11e8-a6d9-e546fe2bba5f",
+ "name": "indexpattern-datasource-current-indexpattern",
+ "type": "index-pattern",
+ },
+ Object {
+ "id": "ff959d40-b880-11e8-a6d9-e546fe2bba5f",
+ "name": "indexpattern-datasource-layer-3b7791e9-326e-40d5-a787-b7594e48d906",
+ "type": "index-pattern",
+ },
+ Object {
+ "id": "90943e30-9a47-11e8-b64d-95841ca0b247",
+ "name": "indexpattern-datasource-layer-9a27f85d-35a9-4246-81b2-48e7ee9b0707",
+ "type": "index-pattern",
+ },
+ Object {
+ "id": "90943e30-9a47-11e8-b64d-95841ca0b247",
+ "name": "filter-index-pattern-0",
+ "type": "index-pattern",
+ },
+ Object {
+ "id": "ff959d40-b880-11e8-a6d9-e546fe2bba5f",
+ "name": "filter-index-pattern-1",
+ "type": "index-pattern",
+ },
+ ],
+ "type": "lens",
+}
+`;
diff --git a/x-pack/plugins/lens/server/migrations.test.ts b/x-pack/plugins/lens/server/migrations.test.ts
index 0541d9636577b..676494dcab619 100644
--- a/x-pack/plugins/lens/server/migrations.test.ts
+++ b/x-pack/plugins/lens/server/migrations.test.ts
@@ -278,4 +278,233 @@ describe('Lens migrations', () => {
expect(result).toEqual(input);
});
});
+
+ describe('7.10.0 references', () => {
+ const context = {} as SavedObjectMigrationContext;
+
+ const example = {
+ attributes: {
+ description: '',
+ expression:
+ 'kibana\n| kibana_context query="{\\"query\\":\\"NOT bytes > 5000\\",\\"language\\":\\"kuery\\"}" \n filters="[{\\"meta\\":{\\"index\\":\\"90943e30-9a47-11e8-b64d-95841ca0b247\\",\\"alias\\":null,\\"negate\\":true,\\"disabled\\":false,\\"type\\":\\"phrase\\",\\"key\\":\\"geo.src\\",\\"params\\":{\\"query\\":\\"CN\\"}},\\"query\\":{\\"match_phrase\\":{\\"geo.src\\":\\"CN\\"}},\\"$state\\":{\\"store\\":\\"appState\\"}},{\\"meta\\":{\\"index\\":\\"ff959d40-b880-11e8-a6d9-e546fe2bba5f\\",\\"alias\\":null,\\"negate\\":true,\\"disabled\\":false,\\"type\\":\\"phrase\\",\\"key\\":\\"geoip.country_iso_code\\",\\"params\\":{\\"query\\":\\"US\\"}},\\"query\\":{\\"match_phrase\\":{\\"geoip.country_iso_code\\":\\"US\\"}},\\"$state\\":{\\"store\\":\\"appState\\"}}]"\n| lens_merge_tables layerIds="9a27f85d-35a9-4246-81b2-48e7ee9b0707"\n layerIds="3b7791e9-326e-40d5-a787-b7594e48d906" \n tables={esaggs index="90943e30-9a47-11e8-b64d-95841ca0b247" metricsAtAllLevels=true partialRows=true includeFormatHints=true aggConfigs="[{\\"id\\":\\"96352896-c508-4fca-90d8-66e9ebfce621\\",\\"enabled\\":true,\\"type\\":\\"terms\\",\\"schema\\":\\"segment\\",\\"params\\":{\\"field\\":\\"geo.src\\",\\"orderBy\\":\\"4ce9b4c7-2ebf-4d48-8669-0ea69d973353\\",\\"order\\":\\"desc\\",\\"size\\":5,\\"otherBucket\\":false,\\"otherBucketLabel\\":\\"Other\\",\\"missingBucket\\":false,\\"missingBucketLabel\\":\\"Missing\\"}},{\\"id\\":\\"4ce9b4c7-2ebf-4d48-8669-0ea69d973353\\",\\"enabled\\":true,\\"type\\":\\"count\\",\\"schema\\":\\"metric\\",\\"params\\":{}}]" | lens_rename_columns idMap="{\\"col-0-96352896-c508-4fca-90d8-66e9ebfce621\\":{\\"label\\":\\"Top values of geo.src\\",\\"dataType\\":\\"string\\",\\"operationType\\":\\"terms\\",\\"scale\\":\\"ordinal\\",\\"sourceField\\":\\"geo.src\\",\\"isBucketed\\":true,\\"params\\":{\\"size\\":5,\\"orderBy\\":{\\"type\\":\\"column\\",\\"columnId\\":\\"4ce9b4c7-2ebf-4d48-8669-0ea69d973353\\"},\\"orderDirection\\":\\"desc\\"},\\"id\\":\\"96352896-c508-4fca-90d8-66e9ebfce621\\"},\\"col-1-4ce9b4c7-2ebf-4d48-8669-0ea69d973353\\":{\\"label\\":\\"Count of records\\",\\"dataType\\":\\"number\\",\\"operationType\\":\\"count\\",\\"isBucketed\\":false,\\"scale\\":\\"ratio\\",\\"sourceField\\":\\"Records\\",\\"id\\":\\"4ce9b4c7-2ebf-4d48-8669-0ea69d973353\\"}}"}\n tables={esaggs index="ff959d40-b880-11e8-a6d9-e546fe2bba5f" metricsAtAllLevels=true partialRows=true includeFormatHints=true aggConfigs="[{\\"id\\":\\"77d8383e-f66e-471e-ae50-c427feedb5ba\\",\\"enabled\\":true,\\"type\\":\\"terms\\",\\"schema\\":\\"segment\\",\\"params\\":{\\"field\\":\\"geoip.country_iso_code\\",\\"orderBy\\":\\"a5c1b82d-51de-4448-a99d-6391432c3a03\\",\\"order\\":\\"desc\\",\\"size\\":5,\\"otherBucket\\":false,\\"otherBucketLabel\\":\\"Other\\",\\"missingBucket\\":false,\\"missingBucketLabel\\":\\"Missing\\"}},{\\"id\\":\\"a5c1b82d-51de-4448-a99d-6391432c3a03\\",\\"enabled\\":true,\\"type\\":\\"count\\",\\"schema\\":\\"metric\\",\\"params\\":{}}]" | lens_rename_columns idMap="{\\"col-0-77d8383e-f66e-471e-ae50-c427feedb5ba\\":{\\"label\\":\\"Top values of geoip.country_iso_code\\",\\"dataType\\":\\"string\\",\\"operationType\\":\\"terms\\",\\"scale\\":\\"ordinal\\",\\"sourceField\\":\\"geoip.country_iso_code\\",\\"isBucketed\\":true,\\"params\\":{\\"size\\":5,\\"orderBy\\":{\\"type\\":\\"column\\",\\"columnId\\":\\"a5c1b82d-51de-4448-a99d-6391432c3a03\\"},\\"orderDirection\\":\\"desc\\"},\\"id\\":\\"77d8383e-f66e-471e-ae50-c427feedb5ba\\"},\\"col-1-a5c1b82d-51de-4448-a99d-6391432c3a03\\":{\\"label\\":\\"Count of records\\",\\"dataType\\":\\"number\\",\\"operationType\\":\\"count\\",\\"isBucketed\\":false,\\"scale\\":\\"ratio\\",\\"sourceField\\":\\"Records\\",\\"id\\":\\"a5c1b82d-51de-4448-a99d-6391432c3a03\\"}}"}\n| lens_xy_chart xTitle="Top values of geo.src" yTitle="Count of records" legend={lens_xy_legendConfig isVisible=true position="right"} fittingFunction="None" \n layers={lens_xy_layer layerId="9a27f85d-35a9-4246-81b2-48e7ee9b0707" hide=false xAccessor="96352896-c508-4fca-90d8-66e9ebfce621" yScaleType="linear" xScaleType="ordinal" isHistogram=false seriesType="bar" accessors="4ce9b4c7-2ebf-4d48-8669-0ea69d973353" columnToLabel="{\\"4ce9b4c7-2ebf-4d48-8669-0ea69d973353\\":\\"Count of records\\"}"}\n layers={lens_xy_layer layerId="3b7791e9-326e-40d5-a787-b7594e48d906" hide=false xAccessor="77d8383e-f66e-471e-ae50-c427feedb5ba" yScaleType="linear" xScaleType="ordinal" isHistogram=false seriesType="bar" accessors="a5c1b82d-51de-4448-a99d-6391432c3a03" columnToLabel="{\\"a5c1b82d-51de-4448-a99d-6391432c3a03\\":\\"Count of records [1]\\"}"}',
+ state: {
+ datasourceMetaData: {
+ filterableIndexPatterns: [
+ { id: '90943e30-9a47-11e8-b64d-95841ca0b247', title: 'kibana_sample_data_logs' },
+ { id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', title: 'kibana_sample_data_ecommerce' },
+ ],
+ },
+ datasourceStates: {
+ indexpattern: {
+ currentIndexPatternId: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
+ layers: {
+ '3b7791e9-326e-40d5-a787-b7594e48d906': {
+ columnOrder: [
+ '77d8383e-f66e-471e-ae50-c427feedb5ba',
+ 'a5c1b82d-51de-4448-a99d-6391432c3a03',
+ ],
+ columns: {
+ '77d8383e-f66e-471e-ae50-c427feedb5ba': {
+ dataType: 'string',
+ isBucketed: true,
+ label: 'Top values of geoip.country_iso_code',
+ operationType: 'terms',
+ params: {
+ orderBy: {
+ columnId: 'a5c1b82d-51de-4448-a99d-6391432c3a03',
+ type: 'column',
+ },
+ orderDirection: 'desc',
+ size: 5,
+ },
+ scale: 'ordinal',
+ sourceField: 'geoip.country_iso_code',
+ },
+ 'a5c1b82d-51de-4448-a99d-6391432c3a03': {
+ dataType: 'number',
+ isBucketed: false,
+ label: 'Count of records',
+ operationType: 'count',
+ scale: 'ratio',
+ sourceField: 'Records',
+ },
+ },
+ indexPatternId: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
+ },
+ '9a27f85d-35a9-4246-81b2-48e7ee9b0707': {
+ columnOrder: [
+ '96352896-c508-4fca-90d8-66e9ebfce621',
+ '4ce9b4c7-2ebf-4d48-8669-0ea69d973353',
+ ],
+ columns: {
+ '4ce9b4c7-2ebf-4d48-8669-0ea69d973353': {
+ dataType: 'number',
+ isBucketed: false,
+ label: 'Count of records',
+ operationType: 'count',
+ scale: 'ratio',
+ sourceField: 'Records',
+ },
+ '96352896-c508-4fca-90d8-66e9ebfce621': {
+ dataType: 'string',
+ isBucketed: true,
+ label: 'Top values of geo.src',
+ operationType: 'terms',
+ params: {
+ orderBy: {
+ columnId: '4ce9b4c7-2ebf-4d48-8669-0ea69d973353',
+ type: 'column',
+ },
+ orderDirection: 'desc',
+ size: 5,
+ },
+ scale: 'ordinal',
+ sourceField: 'geo.src',
+ },
+ },
+ indexPatternId: '90943e30-9a47-11e8-b64d-95841ca0b247',
+ },
+ },
+ },
+ },
+ filters: [
+ {
+ $state: { store: 'appState' },
+ meta: {
+ alias: null,
+ disabled: false,
+ index: '90943e30-9a47-11e8-b64d-95841ca0b247',
+ key: 'geo.src',
+ negate: true,
+ params: { query: 'CN' },
+ type: 'phrase',
+ },
+ query: { match_phrase: { 'geo.src': 'CN' } },
+ },
+ {
+ $state: { store: 'appState' },
+ meta: {
+ alias: null,
+ disabled: false,
+ index: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
+ key: 'geoip.country_iso_code',
+ negate: true,
+ params: { query: 'US' },
+ type: 'phrase',
+ },
+ query: { match_phrase: { 'geoip.country_iso_code': 'US' } },
+ },
+ ],
+ query: { language: 'kuery', query: 'NOT bytes > 5000' },
+ visualization: {
+ fittingFunction: 'None',
+ layers: [
+ {
+ accessors: ['4ce9b4c7-2ebf-4d48-8669-0ea69d973353'],
+ layerId: '9a27f85d-35a9-4246-81b2-48e7ee9b0707',
+ position: 'top',
+ seriesType: 'bar',
+ showGridlines: false,
+ xAccessor: '96352896-c508-4fca-90d8-66e9ebfce621',
+ },
+ {
+ accessors: ['a5c1b82d-51de-4448-a99d-6391432c3a03'],
+ layerId: '3b7791e9-326e-40d5-a787-b7594e48d906',
+ seriesType: 'bar',
+ xAccessor: '77d8383e-f66e-471e-ae50-c427feedb5ba',
+ },
+ ],
+ legend: { isVisible: true, position: 'right' },
+ preferredSeriesType: 'bar',
+ },
+ },
+ title: 'mylens',
+ visualizationType: 'lnsXY',
+ },
+ type: 'lens',
+ };
+
+ it('should remove expression', () => {
+ const result = migrations['7.10.0'](example, context);
+ expect(result.attributes.expression).toBeUndefined();
+ });
+
+ it('should list references for layers', () => {
+ const result = migrations['7.10.0'](example, context);
+ expect(
+ result.references?.find(
+ (ref) => ref.name === 'indexpattern-datasource-layer-3b7791e9-326e-40d5-a787-b7594e48d906'
+ )?.id
+ ).toEqual('ff959d40-b880-11e8-a6d9-e546fe2bba5f');
+ expect(
+ result.references?.find(
+ (ref) => ref.name === 'indexpattern-datasource-layer-9a27f85d-35a9-4246-81b2-48e7ee9b0707'
+ )?.id
+ ).toEqual('90943e30-9a47-11e8-b64d-95841ca0b247');
+ });
+
+ it('should remove index pattern ids from layers', () => {
+ const result = migrations['7.10.0'](example, context);
+ expect(
+ result.attributes.state.datasourceStates.indexpattern.layers[
+ '3b7791e9-326e-40d5-a787-b7594e48d906'
+ ].indexPatternId
+ ).toBeUndefined();
+ expect(
+ result.attributes.state.datasourceStates.indexpattern.layers[
+ '9a27f85d-35a9-4246-81b2-48e7ee9b0707'
+ ].indexPatternId
+ ).toBeUndefined();
+ });
+
+ it('should remove datsource meta data', () => {
+ const result = migrations['7.10.0'](example, context);
+ expect(result.attributes.state.datasourceMetaData).toBeUndefined();
+ });
+
+ it('should list references for filters', () => {
+ const result = migrations['7.10.0'](example, context);
+ expect(result.references?.find((ref) => ref.name === 'filter-index-pattern-0')?.id).toEqual(
+ '90943e30-9a47-11e8-b64d-95841ca0b247'
+ );
+ expect(result.references?.find((ref) => ref.name === 'filter-index-pattern-1')?.id).toEqual(
+ 'ff959d40-b880-11e8-a6d9-e546fe2bba5f'
+ );
+ });
+
+ it('should remove index pattern ids from filters', () => {
+ const result = migrations['7.10.0'](example, context);
+ expect(result.attributes.state.filters[0].meta.index).toBeUndefined();
+ expect(result.attributes.state.filters[0].meta.indexRefName).toEqual(
+ 'filter-index-pattern-0'
+ );
+ expect(result.attributes.state.filters[1].meta.index).toBeUndefined();
+ expect(result.attributes.state.filters[1].meta.indexRefName).toEqual(
+ 'filter-index-pattern-1'
+ );
+ });
+
+ it('should list reference for current index pattern', () => {
+ const result = migrations['7.10.0'](example, context);
+ expect(
+ result.references?.find(
+ (ref) => ref.name === 'indexpattern-datasource-current-indexpattern'
+ )?.id
+ ).toEqual('ff959d40-b880-11e8-a6d9-e546fe2bba5f');
+ });
+
+ it('should remove current index pattern id from datasource state', () => {
+ const result = migrations['7.10.0'](example, context);
+ expect(
+ result.attributes.state.datasourceStates.indexpattern.currentIndexPatternId
+ ).toBeUndefined();
+ });
+
+ it('should produce a valid document', () => {
+ const result = migrations['7.10.0'](example, context);
+ // changes to the outcome of this are critical - this test is a safe guard to not introduce changes accidentally
+ // if this test fails, make extra sure it's expected
+ expect(result).toMatchSnapshot();
+ });
+ });
});
diff --git a/x-pack/plugins/lens/server/migrations.ts b/x-pack/plugins/lens/server/migrations.ts
index d24a3e92cbd9c..fdbfa1e455f60 100644
--- a/x-pack/plugins/lens/server/migrations.ts
+++ b/x-pack/plugins/lens/server/migrations.ts
@@ -6,11 +6,16 @@
import { cloneDeep } from 'lodash';
import { fromExpression, toExpression, Ast, ExpressionFunctionAST } from '@kbn/interpreter/common';
-import { SavedObjectMigrationMap, SavedObjectMigrationFn } from 'src/core/server';
+import {
+ SavedObjectMigrationMap,
+ SavedObjectMigrationFn,
+ SavedObjectReference,
+ SavedObjectUnsanitizedDoc,
+} from 'src/core/server';
+import { Query, Filter } from 'src/plugins/data/public';
+import { PersistableFilter } from '../common';
-interface LensDocShape {
- id?: string;
- type?: string;
+interface LensDocShapePre710 {
visualizationType: string | null;
title: string;
expression: string | null;
@@ -21,18 +26,44 @@ interface LensDocShape {
datasourceStates: {
// This is hardcoded as our only datasource
indexpattern: {
+ currentIndexPatternId: string;
layers: Record<
string,
{
columnOrder: string[];
columns: Record;
+ indexPatternId: string;
}
>;
};
};
visualization: VisualizationState;
- query: unknown;
- filters: unknown[];
+ query: Query;
+ filters: Filter[];
+ };
+}
+
+interface LensDocShape {
+ id?: string;
+ type?: string;
+ visualizationType: string | null;
+ title: string;
+ state: {
+ datasourceStates: {
+ // This is hardcoded as our only datasource
+ indexpattern: {
+ layers: Record<
+ string,
+ {
+ columnOrder: string[];
+ columns: Record;
+ }
+ >;
+ };
+ };
+ visualization: VisualizationState;
+ query: Query;
+ filters: PersistableFilter[];
};
}
@@ -55,7 +86,10 @@ interface XYStatePost77 {
* Removes the `lens_auto_date` subexpression from a stored expression
* string. For example: aggConfigs={lens_auto_date aggConfigs="JSON string"}
*/
-const removeLensAutoDate: SavedObjectMigrationFn = (doc, context) => {
+const removeLensAutoDate: SavedObjectMigrationFn = (
+ doc,
+ context
+) => {
const expression = doc.attributes.expression;
if (!expression) {
return doc;
@@ -112,7 +146,10 @@ const removeLensAutoDate: SavedObjectMigrationFn = (
/**
* Adds missing timeField arguments to esaggs in the Lens expression
*/
-const addTimeFieldToEsaggs: SavedObjectMigrationFn = (doc, context) => {
+const addTimeFieldToEsaggs: SavedObjectMigrationFn = (
+ doc,
+ context
+) => {
const expression = doc.attributes.expression;
if (!expression) {
return doc;
@@ -174,14 +211,14 @@ const addTimeFieldToEsaggs: SavedObjectMigrationFn =
};
const removeInvalidAccessors: SavedObjectMigrationFn<
- LensDocShape,
- LensDocShape
+ LensDocShapePre710,
+ LensDocShapePre710
> = (doc) => {
const newDoc = cloneDeep(doc);
if (newDoc.attributes.visualizationType === 'lnsXY') {
const datasourceLayers = newDoc.attributes.state.datasourceStates.indexpattern.layers || {};
const xyState = newDoc.attributes.state.visualization;
- (newDoc.attributes as LensDocShape<
+ (newDoc.attributes as LensDocShapePre710<
XYStatePost77
>).state.visualization.layers = xyState.layers.map((layer: XYLayerPre77) => {
const layerId = layer.layerId;
@@ -197,9 +234,86 @@ const removeInvalidAccessors: SavedObjectMigrationFn<
return newDoc;
};
+const extractReferences: SavedObjectMigrationFn = ({
+ attributes,
+ references,
+ ...docMeta
+}) => {
+ const savedObjectReferences: SavedObjectReference[] = [];
+ // add currently selected index pattern to reference list
+ savedObjectReferences.push({
+ type: 'index-pattern',
+ id: attributes.state.datasourceStates.indexpattern.currentIndexPatternId,
+ name: 'indexpattern-datasource-current-indexpattern',
+ });
+
+ // add layer index patterns to list and remove index pattern ids from layers
+ const persistableLayers: Record<
+ string,
+ Omit<
+ LensDocShapePre710['state']['datasourceStates']['indexpattern']['layers'][string],
+ 'indexPatternId'
+ >
+ > = {};
+ Object.entries(attributes.state.datasourceStates.indexpattern.layers).forEach(
+ ([layerId, { indexPatternId, ...persistableLayer }]) => {
+ savedObjectReferences.push({
+ type: 'index-pattern',
+ id: indexPatternId,
+ name: `indexpattern-datasource-layer-${layerId}`,
+ });
+ persistableLayers[layerId] = persistableLayer;
+ }
+ );
+
+ // add filter index patterns to reference list and remove index pattern ids from filter definitions
+ const persistableFilters = attributes.state.filters.map((filterRow, i) => {
+ if (!filterRow.meta || !filterRow.meta.index) {
+ return filterRow;
+ }
+ const refName = `filter-index-pattern-${i}`;
+ savedObjectReferences.push({
+ name: refName,
+ type: 'index-pattern',
+ id: filterRow.meta.index,
+ });
+ return {
+ ...filterRow,
+ meta: {
+ ...filterRow.meta,
+ indexRefName: refName,
+ index: undefined,
+ },
+ };
+ });
+
+ // put together new saved object format
+ const newDoc: SavedObjectUnsanitizedDoc = {
+ ...docMeta,
+ references: savedObjectReferences,
+ attributes: {
+ visualizationType: attributes.visualizationType,
+ title: attributes.title,
+ state: {
+ datasourceStates: {
+ indexpattern: {
+ layers: persistableLayers,
+ },
+ },
+ visualization: attributes.state.visualization,
+ query: attributes.state.query,
+ filters: persistableFilters,
+ },
+ },
+ };
+
+ return newDoc;
+};
+
export const migrations: SavedObjectMigrationMap = {
'7.7.0': removeInvalidAccessors,
// The order of these migrations matter, since the timefield migration relies on the aggConfigs
// sitting directly on the esaggs as an argument and not a nested function (which lens_auto_date was).
'7.8.0': (doc, context) => addTimeFieldToEsaggs(removeLensAutoDate(doc, context), context),
+ '7.10.0': extractReferences,
};