From 3ad79cd620eb191860b5f2ae17729b23bfc6dfad Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Thu, 6 Aug 2020 10:19:10 +0200 Subject: [PATCH 01/16] clear out all attribute properties before updating --- .../public/persistence/saved_object_store.ts | 45 ++++++++++--------- 1 file changed, 25 insertions(+), 20 deletions(-) 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 7632be3d82046..59ead53956a8d 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,7 @@ * you may not use this file except in compliance with the Elastic License. */ -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { SavedObjectAttributes } from 'kibana/server'; +import { SavedObjectAttributes, SavedObjectsClientContract } from 'kibana/public'; import { Query, Filter } from '../../../../../src/plugins/data/public'; export interface Document { @@ -28,20 +27,6 @@ export interface Document { export const DOC_TYPE = 'lens'; -interface SavedObjectClient { - create: (type: string, object: SavedObjectAttributes) => Promise<{ id: string }>; - update: (type: string, id: string, object: SavedObjectAttributes) => Promise<{ id: string }>; - get: ( - type: string, - id: string - ) => Promise<{ - id: string; - type: string; - attributes: SavedObjectAttributes; - error?: { statusCode: number; message: string }; - }>; -} - export interface DocumentSaver { save: (vis: Document) => Promise<{ id: string }>; } @@ -53,9 +38,9 @@ export interface DocumentLoader { export type SavedObjectStore = DocumentLoader & DocumentSaver; export class SavedObjectIndexStore implements SavedObjectStore { - private client: SavedObjectClient; + private client: SavedObjectsClientContract; - constructor(client: SavedObjectClient) { + constructor(client: SavedObjectsClientContract) { this.client = client; } @@ -64,13 +49,33 @@ export class SavedObjectIndexStore implements SavedObjectStore { // 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.client.update(DOC_TYPE, id, attributes) + ? this.safeUpdate(id, attributes) : this.client.create(DOC_TYPE, attributes)); return { ...vis, id: result.id }; } + // As Lens is using an object to store its attributes, using the update API + // will merge the new attribute object with the old one, not overwriting deleted + // keys. As Lens is using objects as maps in various places, this is a problem because + // 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) { + 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 }, + ]) + ).savedObjects[1]; + } + async load(id: string): Promise { const { type, attributes, error } = await this.client.get(DOC_TYPE, id); @@ -79,7 +84,7 @@ export class SavedObjectIndexStore implements SavedObjectStore { } return { - ...attributes, + ...(attributes as SavedObjectAttributes), id, type, } as Document; From 94293c2a81f0df5720ef1fca66108dbb12686cbf Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Thu, 6 Aug 2020 10:57:30 +0200 Subject: [PATCH 02/16] fix tests --- .../persistence/saved_object_store.test.ts | 47 +++++++++++++------ 1 file changed, 33 insertions(+), 14 deletions(-) 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 f7caac6549389..f8f8d889233a7 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 @@ -4,19 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ +import { SavedObjectsClientContract, SavedObjectsBulkUpdateObject } from 'kibana/public'; import { SavedObjectIndexStore } from './saved_object_store'; describe('LensStore', () => { function testStore(testId?: string) { const client = { create: jest.fn(() => Promise.resolve({ id: testId || 'testid' })), - update: jest.fn((_type: string, id: string) => Promise.resolve({ id })), + bulkUpdate: jest.fn(([{ id }]: SavedObjectsBulkUpdateObject[]) => + Promise.resolve({ savedObjects: [{ id }, { id }] }) + ), get: jest.fn(), }; return { client, - store: new SavedObjectIndexStore(client), + store: new SavedObjectIndexStore((client as unknown) as SavedObjectsClientContract), }; } @@ -108,19 +111,35 @@ describe('LensStore', () => { }, }); - expect(client.update).toHaveBeenCalledTimes(1); - expect(client.update).toHaveBeenCalledWith('lens', 'Gandalf', { - 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' }, - filters: [], + expect(client.bulkUpdate).toHaveBeenCalledTimes(1); + expect(client.bulkUpdate).toHaveBeenCalledWith([ + { + type: 'lens', + id: 'Gandalf', + attributes: { + title: null, + visualizationType: null, + expression: null, + state: null, + }, }, - }); + { + type: 'lens', + id: 'Gandalf', + 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' }, + filters: [], + }, + }, + }, + ]); }); }); From ff69693b8932405de4298635153f8c8802f43313 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Thu, 6 Aug 2020 15:32:25 +0200 Subject: [PATCH 03/16] start implementing saved object references --- .../public/persistence/saved_object_store.ts | 20 +++++++++++-------- x-pack/plugins/lens/public/types.ts | 9 +++++---- 2 files changed, 17 insertions(+), 12 deletions(-) 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..c568d5d066f61 100644 --- a/x-pack/plugins/lens/public/persistence/saved_object_store.ts +++ b/x-pack/plugins/lens/public/persistence/saved_object_store.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectAttributes, SavedObjectsClientContract } from 'kibana/public'; +import { SavedObjectAttributes, SavedObjectsClientContract, SavedObjectReference } from 'kibana/public'; import { Query, Filter } from '../../../../../src/plugins/data/public'; export interface Document { @@ -23,6 +23,7 @@ export interface Document { query: Query; filters: Filter[]; }; + references: SavedObjectReference[]; } export const DOC_TYPE = 'lens'; @@ -45,14 +46,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 +66,21 @@ 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 +88,7 @@ export class SavedObjectIndexStore implements SavedObjectStore { return { ...(attributes as SavedObjectAttributes), + references, id, type, } as Document; diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index c7bda65cd1327..feaf735f2f2a5 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -22,6 +22,7 @@ import { TriggerContext, VALUE_CLICK_TRIGGER, } from '../../../../src/plugins/ui_actions/public'; +import { SavedObjectReference } from 'kibana/public'; export type ErrorCallback = (e: { message: string }) => void; @@ -146,10 +147,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; @@ -418,11 +419,11 @@ export interface Visualization { * - Loadingn from a saved visualization * - When using suggestions, the suggested state is passed in */ - initialize: (frame: FramePublicAPI, state?: P) => T; + initialize: (frame: FramePublicAPI, state?: P, savedObjectReferences?: SavedObjectReference[]) => T; /** * Can remove any state that should not be persisted to saved object, such as UI state */ - getPersistableState: (state: T) => P; + getPersistableState: (state: T) => { state: P, savedObjectReferences: SavedObjectReference[] }; /** * Visualizations must provide at least one type for the chart switcher, From a16dad98d598f66d2fd38e5e0664148f66b7f1c1 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Mon, 10 Aug 2020 09:17:39 +0200 Subject: [PATCH 04/16] implement some more things --- .../editor_frame_service/editor_frame/save.ts | 29 ++++++++++--------- .../public/persistence/saved_object_store.ts | 3 +- 2 files changed, 17 insertions(+), 15 deletions(-) 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..fa413cfd67762 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 @@ -8,8 +8,8 @@ import _ from 'lodash'; import { toExpression } from '@kbn/interpreter/target/common'; import { EditorFrameState } from './state_management'; import { Document } from '../../persistence/saved_object_store'; -import { buildExpression } from './expression_helpers'; import { Datasource, Visualization, FramePublicAPI } from '../../types'; +import { SavedObjectReference } from 'kibana/public'; export interface Props { activeDatasources: Record; @@ -24,18 +24,12 @@ export function getSavedObjectFormat({ visualization, framePublicAPI, }: Props): Document { - const expression = buildExpression({ - visualization, - visualizationState: state.visualization.state, - datasourceMap: activeDatasources, - datasourceStates: state.datasourceStates, - framePublicAPI, - removeDateRange: true, - }); - const datasourceStates: Record = {}; + const references: SavedObjectReference[] = [] Object.entries(activeDatasources).forEach(([id, datasource]) => { - datasourceStates[id] = datasource.getPersistableState(state.datasourceStates[id].state); + const { state: persistableState, savedObjectReferences } = datasource.getPersistableState(state.datasourceStates[id].state); + datasourceStates[id] = persistableState; + references.push(...savedObjectReferences); }); const filterableIndexPatterns: Array<{ id: string; title: string }> = []; @@ -44,6 +38,15 @@ export function getSavedObjectFormat({ ...datasource.getMetaData(state.datasourceStates[id].state).filterableIndexPatterns ); }); + + const { state: persistableVisualizationState, savedObjectReferences } = visualization.getPersistableState(state.visualization.state); + references.push(...savedObjectReferences); + + const uniqueFilterableIndexPatternIds = _.uniqBy(filterableIndexPatterns, 'id').map(({ id }) => id); + + uniqueFilterableIndexPatternIds.forEach((id, index) => { + references.push({ type: 'index-pattern', id, name: `filterable-index-pattern-${index}` }); + }); return { id: state.persistedId, @@ -51,15 +54,15 @@ export function getSavedObjectFormat({ description: state.description, type: 'lens', visualizationType: state.visualization.activeId, - expression: expression ? toExpression(expression) : '', state: { datasourceStates, datasourceMetaData: { filterableIndexPatterns: _.uniqBy(filterableIndexPatterns, 'id'), }, - visualization: visualization.getPersistableState(state.visualization.state), + visualization: persistableVisualizationState, query: framePublicAPI.query, filters: framePublicAPI.filters, }, + references, }; } 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 c568d5d066f61..78f3760263daa 100644 --- a/x-pack/plugins/lens/public/persistence/saved_object_store.ts +++ b/x-pack/plugins/lens/public/persistence/saved_object_store.ts @@ -13,10 +13,9 @@ export interface Document { visualizationType: string | null; title: string; description?: string; - expression: string | null; state: { datasourceMetaData: { - filterableIndexPatterns: Array<{ id: string; title: string }>; + filterableIndexPatterns: Array<{ id: string; }>; }; datasourceStates: Record; visualization: unknown; From 358659b577f941a85bd68d202dd71aa042280ec1 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Mon, 10 Aug 2020 15:12:49 +0200 Subject: [PATCH 05/16] add reference stuff --- x-pack/plugins/lens/public/app_plugin/app.tsx | 33 ++++---- .../editor_frame/editor_frame.tsx | 35 ++++----- .../editor_frame_service/editor_frame/save.ts | 77 +++++++++++++------ .../indexpattern_datasource/indexpattern.tsx | 21 +++-- .../public/indexpattern_datasource/loader.ts | 59 +++++++++++++- .../public/indexpattern_datasource/types.ts | 9 ++- .../public/persistence/filter_references.ts | 64 +++++++++++++++ .../plugins/lens/public/persistence/index.ts | 1 + .../public/persistence/saved_object_store.ts | 23 ++++-- x-pack/plugins/lens/public/types.ts | 11 ++- 10 files changed, 252 insertions(+), 81 deletions(-) create mode 100644 x-pack/plugins/lens/public/persistence/filter_references.ts diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx index 4a8694862642b..957b2fed30968 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.tsx @@ -26,7 +26,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'; @@ -55,6 +55,7 @@ interface State { query: Query; filters: Filter[]; savedQuery?: SavedQuery; + isSaveable: boolean; } export function App({ @@ -96,6 +97,7 @@ export function App({ originatingApp, filters: data.query.filterManager.getFilters(), indicateNoData: false, + isSaveable: false, }; }); @@ -118,11 +120,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 @@ -177,9 +175,7 @@ export function App({ // 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, lastKnownDoc) ) { return actions.confirm( i18n.translate('xpack.lens.app.unsavedWorkMessage', { @@ -230,7 +226,9 @@ export function App({ ) .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, @@ -283,7 +281,7 @@ export function App({ return; } const [pinnedFilters, appFilters] = _.partition( - lastKnownDoc.state?.filters, + injectFilterReferences(lastKnownDoc.state?.filters || [], lastKnownDoc.references), esFilters.isFilterPinned ); const lastDocWithoutPinned = pinnedFilters?.length @@ -389,7 +387,7 @@ export function App({ emphasize: true, iconType: 'check', run: () => { - if (isSaveable && lastKnownDoc) { + if (savingPermitted) { runSave({ newTitle: lastKnownDoc.title, newCopyOnSave: false, @@ -399,7 +397,7 @@ export function App({ } }, testId: 'lnsApp_saveAndReturnButton', - disableButton: !isSaveable, + disableButton: !savingPermitted, }, ] : []), @@ -414,12 +412,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" @@ -500,7 +498,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 })); } 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..b8adeb986d182 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 @@ -47,6 +47,7 @@ export interface EditorFrameProps { onChange: (arg: { filterableIndexPatterns: DatasourceMetaData['filterableIndexPatterns']; doc: Document; + isSaveable: boolean; }) => void; showNoDataPopover: () => void; } @@ -165,7 +166,19 @@ 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.references + ) + : props.doc.state.visualization, + }, + }, }); } else { dispatch({ @@ -206,23 +219,7 @@ 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 - ); - }) - .forEach(([id, datasource]) => { - indexPatterns.push( - ...datasource.getMetaData(state.datasourceStates[id].state).filterableIndexPatterns - ); - }); - - const doc = getSavedObjectFormat({ + const { filterableIndexPatterns, doc, isSaveable } = getSavedObjectFormat({ activeDatasources: Object.keys(state.datasourceStates).reduce( (datasourceMap, datasourceId) => ({ ...datasourceMap, @@ -235,7 +232,7 @@ export function EditorFrame(props: EditorFrameProps) { framePublicAPI, }); - props.onChange({ filterableIndexPatterns: indexPatterns, doc }); + props.onChange({ filterableIndexPatterns, doc, isSaveable }); }, // eslint-disable-next-line react-hooks/exhaustive-deps [ 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 fa413cfd67762..32abb0ecd6966 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 { Datasource, Visualization, FramePublicAPI } from '../../types'; -import { SavedObjectReference } from 'kibana/public'; +import { Datasource, Visualization, FramePublicAPI, DatasourceMetaData } from '../../types'; +import { extractFilterReferences } from '../../persistence'; +import { buildExpression } from './expression_helpers'; export interface Props { activeDatasources: Record; @@ -23,11 +24,17 @@ export function getSavedObjectFormat({ state, visualization, framePublicAPI, -}: Props): Document { +}: Props): { + doc: Document; + filterableIndexPatterns: DatasourceMetaData['filterableIndexPatterns']; + isSaveable: boolean; +} { const datasourceStates: Record = {}; - const references: SavedObjectReference[] = [] + const references: SavedObjectReference[] = []; Object.entries(activeDatasources).forEach(([id, datasource]) => { - const { state: persistableState, savedObjectReferences } = datasource.getPersistableState(state.datasourceStates[id].state); + const { state: persistableState, savedObjectReferences } = datasource.getPersistableState( + state.datasourceStates[id].state + ); datasourceStates[id] = persistableState; references.push(...savedObjectReferences); }); @@ -38,31 +45,55 @@ export function getSavedObjectFormat({ ...datasource.getMetaData(state.datasourceStates[id].state).filterableIndexPatterns ); }); - - const { state: persistableVisualizationState, savedObjectReferences } = visualization.getPersistableState(state.visualization.state); + + const { + state: persistableVisualizationState, + savedObjectReferences, + } = visualization.getPersistableState(state.visualization.state); references.push(...savedObjectReferences); - - const uniqueFilterableIndexPatternIds = _.uniqBy(filterableIndexPatterns, 'id').map(({ id }) => id); + + const uniqueFilterableIndexPatternIds = _.uniqBy(filterableIndexPatterns, 'id').map( + ({ id }) => id + ); uniqueFilterableIndexPatternIds.forEach((id, index) => { references.push({ type: 'index-pattern', id, name: `filterable-index-pattern-${index}` }); }); + 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, + }); + return { - id: state.persistedId, - title: state.title, - description: state.description, - type: 'lens', - visualizationType: state.visualization.activeId, - 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, + datasourceMetaData: { + filterableIndexPatterns: _.uniqBy(filterableIndexPatterns, 'id'), + }, + visualization: persistableVisualizationState, + query: framePublicAPI.query, + filters: persistableFilters, }, - visualization: persistableVisualizationState, - query: framePublicAPI.query, - filters: framePublicAPI.filters, + references, }, - references, + filterableIndexPatterns, + isSaveable: expression !== null, }; } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index e9d095bfbcef1..687f7b413bf4d 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, @@ -123,17 +128,21 @@ 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, }); }, - getPersistableState({ currentIndexPatternId, layers }: IndexPatternPrivateState) { - return { currentIndexPatternId, layers }; + getPersistableState(state: IndexPatternPrivateState) { + return extractReferences(state); }, insertLayer(state: IndexPatternPrivateState, newLayerId: string) { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts index 20e7bec6db131..80c0d8d4305b1 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts @@ -6,7 +6,12 @@ import _ from 'lodash'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; -import { SavedObjectsClientContract, SavedObjectAttributes, HttpSetup } from 'kibana/public'; +import { + SavedObjectsClientContract, + SavedObjectAttributes, + HttpSetup, + SavedObjectReference, +} from 'kibana/public'; import { SimpleSavedObject } from 'kibana/public'; import { StateSetter } from '../types'; import { @@ -15,6 +20,7 @@ import { IndexPatternPersistedState, IndexPatternPrivateState, IndexPatternField, + IndexPatternLayer, } from './types'; import { updateLayerIndexPattern } from './state_helpers'; import { DateRange, ExistingFields } from '../../common/types'; @@ -82,13 +88,57 @@ 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, }: { - state?: IndexPatternPersistedState; + persistedState?: IndexPatternPersistedState; + references?: SavedObjectReference[]; savedObjectsClient: SavedObjectsClient; defaultIndexPatternId?: string; storage: IStorageWrapper; @@ -96,6 +146,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 2a9b3f452d991..55bf5dedad047 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/types.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/types.ts @@ -39,11 +39,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; @@ -53,7 +54,7 @@ export type IndexPatternPrivateState = IndexPatternPersistedState & { existingFields: Record>; isFirstExistenceFetch: boolean; existenceFetchFailed?: boolean; -}; +} export interface IndexPatternRef { id: string; 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..e81133b455e22 --- /dev/null +++ b/x-pack/plugins/lens/public/persistence/filter_references.ts @@ -0,0 +1,64 @@ +/* + * 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 { FilterMeta } from 'src/plugins/data/common'; + +export interface PersistableFilterMeta extends FilterMeta { + indexRefName?: string; +} + +export interface PersistableFilter extends Filter { + meta: PersistableFilterMeta; +} + +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, + }; + }); +} 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.ts b/x-pack/plugins/lens/public/persistence/saved_object_store.ts index 78f3760263daa..12c05c8dee046 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, SavedObjectReference } 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 './filter_references'; export interface Document { id?: string; @@ -15,12 +20,12 @@ export interface Document { description?: string; state: { datasourceMetaData: { - filterableIndexPatterns: Array<{ id: string; }>; + filterableIndexPatterns: Array<{ id: string }>; }; datasourceStates: Record; visualization: unknown; query: Query; - filters: Filter[]; + filters: PersistableFilter[]; }; references: SavedObjectReference[]; } @@ -53,8 +58,8 @@ export class SavedObjectIndexStore implements SavedObjectStore { const result = await (id ? this.safeUpdate(id, attributes, references) : this.client.create(DOC_TYPE, attributes, { - references - })); + references, + })); return { ...vis, id: result.id }; } @@ -65,7 +70,11 @@ 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, references: SavedObjectReference[]) { + private async safeUpdate( + id: string, + attributes: SavedObjectAttributes, + references: SavedObjectReference[] + ) { const resetAttributes: SavedObjectAttributes = {}; Object.keys(attributes).forEach((key) => { resetAttributes[key] = null; diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index feaf735f2f2a5..79122da0f0503 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, @@ -22,7 +23,6 @@ import { TriggerContext, VALUE_CLICK_TRIGGER, } from '../../../../src/plugins/ui_actions/public'; -import { SavedObjectReference } from 'kibana/public'; export type ErrorCallback = (e: { message: string }) => void; @@ -47,6 +47,7 @@ export interface EditorFrameProps { onChange: (newState: { filterableIndexPatterns: DatasourceMetaData['filterableIndexPatterns']; doc: Document; + isSaveable: boolean; }) => void; showNoDataPopover: () => void; } @@ -419,11 +420,15 @@ export interface Visualization { * - Loadingn from a saved visualization * - When using suggestions, the suggested state is passed in */ - initialize: (frame: FramePublicAPI, state?: P, savedObjectReferences?: SavedObjectReference[]) => T; + initialize: ( + frame: FramePublicAPI, + state?: P, + savedObjectReferences?: SavedObjectReference[] + ) => T; /** * Can remove any state that should not be persisted to saved object, such as UI state */ - getPersistableState: (state: T) => { state: P, savedObjectReferences: SavedObjectReference[] }; + getPersistableState: (state: T) => { state: P; savedObjectReferences: SavedObjectReference[] }; /** * Visualizations must provide at least one type for the chart switcher, From 781488e1e1955675695cae695857c3acf95b210d Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Tue, 11 Aug 2020 14:45:15 +0200 Subject: [PATCH 06/16] make references work everywhere --- x-pack/plugins/lens/public/app_plugin/app.tsx | 19 +++-- .../visualization.test.tsx | 14 --- .../datatable_visualization/visualization.tsx | 7 +- .../editor_frame/editor_frame.tsx | 8 +- .../editor_frame_service/editor_frame/save.ts | 22 ++--- .../embeddable/embeddable.tsx | 14 ++- .../embeddable/embeddable_factory.ts | 85 ++++++++++++++++--- .../public/editor_frame_service/service.tsx | 10 ++- .../indexpattern_datasource/indexpattern.tsx | 7 +- .../metric_visualization.test.ts | 6 -- .../metric_visualization.tsx | 6 +- .../lens/public/metric_visualization/types.ts | 2 - .../public/persistence/filter_references.ts | 15 ++++ .../public/persistence/saved_object_store.ts | 2 +- .../pie_visualization/pie_visualization.tsx | 4 +- x-pack/plugins/lens/public/types.ts | 18 +--- .../lens/public/xy_visualization/types.ts | 1 - .../xy_visualization/xy_visualization.test.ts | 6 -- .../xy_visualization/xy_visualization.tsx | 6 +- 19 files changed, 144 insertions(+), 108 deletions(-) diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx index 957b2fed30968..dfbeea556957f 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.tsx @@ -26,7 +26,12 @@ import { OnSaveProps, checkForDuplicateTitle, } from '../../../../../src/plugins/saved_objects/public'; -import { Document, SavedObjectStore, injectFilterReferences } from '../persistence'; +import { + Document, + SavedObjectStore, + injectFilterReferences, + getFilterableIndexPatternIds, +} from '../persistence'; import { EditorFrameInstance } from '../types'; import { NativeRenderer } from '../native_renderer'; import { trackUiEvent } from '../lens_ui_telemetry'; @@ -220,7 +225,7 @@ export function App({ .load(docId) .then((doc) => { getAllIndexPatterns( - doc.state.datasourceMetaData.filterableIndexPatterns, + getFilterableIndexPatternIds(doc), data.indexPatterns, core.notifications ) @@ -244,7 +249,7 @@ export function App({ redirectTo(); }); }) - .catch(() => { + .catch((e) => { setState((s) => ({ ...s, isLoading: false })); core.notifications.toasts.addDanger( @@ -509,8 +514,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) ) ) { @@ -551,12 +556,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 e18190b6c2d69..1f655dca391a4 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx @@ -48,20 +48,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 = { diff --git a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx index e4b371143594a..dd721a7c0d32c 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx @@ -24,10 +24,7 @@ function newLayerState(layerId: string): LayerState { }; } -export const datatableVisualization: Visualization< - DatatableVisualizationState, - DatatableVisualizationState -> = { +export const datatableVisualization: Visualization = { id: 'lnsDatatable', visualizationTypes: [ @@ -74,8 +71,6 @@ export const datatableVisualization: Visualization< ); }, - getPersistableState: (state) => state, - getSuggestions({ table, state, 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 b8adeb986d182..089c4f68d79b0 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 @@ -74,7 +74,10 @@ export function EditorFrame(props: EditorFrameProps) { state.datasourceStates[datasourceId].isLoading ) { datasource - .initialize(state.datasourceStates[datasourceId].state || undefined) + .initialize( + state.datasourceStates[datasourceId].state || undefined, + props.doc?.references + ) .then((datasourceState) => { if (!isUnmounted) { dispatch({ @@ -173,8 +176,7 @@ export function EditorFrame(props: EditorFrameProps) { visualization: props.doc.visualizationType ? props.visualizationMap[props.doc.visualizationType].initialize( framePublicAPI, - props.doc.state.visualization, - props.doc.references + props.doc.state.visualization ) : props.doc.state.visualization, }, 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 32abb0ecd6966..1cdce18ed003c 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 @@ -9,7 +9,7 @@ import { SavedObjectReference } from 'kibana/public'; import { EditorFrameState } from './state_management'; import { Document } from '../../persistence/saved_object_store'; import { Datasource, Visualization, FramePublicAPI, DatasourceMetaData } from '../../types'; -import { extractFilterReferences } from '../../persistence'; +import { extractFilterReferences, filterableIndexPatternIdsToReferences } from '../../persistence'; import { buildExpression } from './expression_helpers'; export interface Props { @@ -39,26 +39,16 @@ export function getSavedObjectFormat({ references.push(...savedObjectReferences); }); - const filterableIndexPatterns: Array<{ id: string; title: string }> = []; + const filterableIndexPatterns: string[] = []; Object.entries(activeDatasources).forEach(([id, datasource]) => { filterableIndexPatterns.push( ...datasource.getMetaData(state.datasourceStates[id].state).filterableIndexPatterns ); }); - const { - state: persistableVisualizationState, - savedObjectReferences, - } = visualization.getPersistableState(state.visualization.state); - references.push(...savedObjectReferences); + const uniqueFilterableIndexPatternIds = _.uniq(filterableIndexPatterns); - const uniqueFilterableIndexPatternIds = _.uniqBy(filterableIndexPatterns, 'id').map( - ({ id }) => id - ); - - uniqueFilterableIndexPatternIds.forEach((id, index) => { - references.push({ type: 'index-pattern', id, name: `filterable-index-pattern-${index}` }); - }); + references.push(...filterableIndexPatternIdsToReferences(uniqueFilterableIndexPatternIds)); const { persistableFilters, references: filterReferences } = extractFilterReferences( framePublicAPI.filters @@ -85,9 +75,9 @@ export function getSavedObjectFormat({ state: { datasourceStates, datasourceMetaData: { - filterableIndexPatterns: _.uniqBy(filterableIndexPatterns, 'id'), + numberFilterableIndexPatterns: uniqueFilterableIndexPatternIds.length, }, - visualization: persistableVisualizationState, + visualization: state.visualization.state, query: framePublicAPI.query, filters: persistableFilters, }, 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..c2d4f94ebd947 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 @@ -34,6 +34,7 @@ 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,6 +57,7 @@ export class Embeddable extends AbstractEmbeddable this.onContainerStateChanged(input)); this.onContainerStateChanged(initialInput); @@ -149,7 +159,7 @@ export class Embeddable extends AbstractEmbeddable, diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts index 9a901d3631ec3..1de24d76c957e 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts @@ -7,6 +7,7 @@ import { Capabilities, HttpSetup, SavedObjectsClientContract } from 'kibana/public'; import { i18n } from '@kbn/i18n'; import { RecursiveReadonly } from '@kbn/utility-types'; +import { toExpression } from '@kbn/interpreter/target/common'; import { IndexPatternsContract, IndexPattern, @@ -20,9 +21,11 @@ import { IContainer, } from '../../../../../../src/plugins/embeddable/public'; import { Embeddable } from './embeddable'; -import { SavedObjectIndexStore, DOC_TYPE } from '../../persistence'; +import { SavedObjectIndexStore, DOC_TYPE, getFilterableIndexPatternIds } from '../../persistence'; import { getEditPath } from '../../../common'; import { UiActionsStart } from '../../../../../../src/plugins/ui_actions/public'; +import { buildExpression } from '../editor_frame/expression_helpers'; +import { Datasource, Visualization, DatasourcePublicAPI, FramePublicAPI } from '../../types'; interface StartServices { timefilter: TimefilterContract; @@ -32,6 +35,8 @@ interface StartServices { expressionRenderer: ReactExpressionRendererType; indexPatternService: IndexPatternsContract; uiActions?: UiActionsStart; + datasources: Record>; + visualizations: Record>; } export class EmbeddableFactory implements EmbeddableFactoryDefinition { @@ -73,28 +78,83 @@ export class EmbeddableFactory implements EmbeddableFactoryDefinition { timefilter, expressionRenderer, uiActions, + datasources, + visualizations, } = await this.getStartServices(); const store = new SavedObjectIndexStore(savedObjectsClient); const savedVis = await store.load(savedObjectId); - const promises = savedVis.state.datasourceMetaData.filterableIndexPatterns.map( - async ({ id }) => { - try { - return await indexPatternService.get(id); - } catch (error) { - // Unable to load index pattern, ignore error as the index patterns are only used to - // configure the filter and query bar - there is still a good chance to get the visualization - // to show. - return null; - } + const promises = getFilterableIndexPatternIds(savedVis).map(async (id) => { + try { + return await indexPatternService.get(id); + } catch (error) { + // Unable to load index pattern, ignore error as the index patterns are only used to + // configure the filter and query bar - there is still a good chance to get the visualization + // to show. + return null; } - ); + }); const indexPatterns = ( await Promise.all(promises) ).filter((indexPattern: IndexPattern | null): indexPattern is IndexPattern => Boolean(indexPattern) ); + const datasourceStates: Record = {}; + + await Promise.all( + Object.entries(datasources).map(([datasourceId, datasource]) => { + if (savedVis.state.datasourceStates[datasourceId]) { + return datasource + .initialize(savedVis.state.datasourceStates[datasourceId], savedVis.references) + .then((datasourceState) => { + datasourceStates[datasourceId] = { isLoading: false, state: datasourceState }; + }); + } + }) + ); + + const datasourceLayers: Record = {}; + Object.keys(datasources) + .filter((id) => datasourceStates[id]) + .forEach((id) => { + const datasourceState = datasourceStates[id].state; + const datasource = datasources[id]; + + const layers = datasource.getLayers(datasourceState); + layers.forEach((layer) => { + datasourceLayers[layer] = datasource.getPublicAPI({ + state: datasourceState, + layerId: layer, + dateRange: { fromDate: 'now', toDate: 'now' }, + }); + }); + }); + + const framePublicAPI: FramePublicAPI = { + datasourceLayers, + dateRange: { fromDate: 'now', toDate: 'now' }, + query: savedVis.state.query, + filters: savedVis.state.filters, + + addNewLayer() { + throw new Error('adding new layer is not allowed in embedded mode'); + }, + + removeLayers() { + throw new Error('removing layers is not allowed in embedded mode'); + }, + }; + + const expression = buildExpression({ + visualization: visualizations[savedVis.visualizationType!], + visualizationState: savedVis.state.visualization, + datasourceMap: datasources, + datasourceStates, + framePublicAPI, + removeDateRange: true, + }); + return new Embeddable( timefilter, expressionRenderer, @@ -105,6 +165,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/service.tsx b/x-pack/plugins/lens/public/editor_frame_service/service.tsx index 47339373b6d1a..f66d6bc1ede2a 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/service.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/service.tsx @@ -8,6 +8,8 @@ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { I18nProvider } from '@kbn/i18n/react'; import { CoreSetup, CoreStart } from 'kibana/public'; +import { Observable, Subject } from 'rxjs'; +import { first } from 'rxjs/operators'; import { ExpressionsSetup, ExpressionsStart } from '../../../../../src/plugins/expressions/public'; import { EmbeddableSetup, EmbeddableStart } from '../../../../../src/plugins/embeddable/public'; import { @@ -68,6 +70,10 @@ export class EditorFrameService { const getStartServices = async () => { const [coreStart, deps] = await core.getStartServices(); + const [resolvedDatasources, resolvedVisualizations] = await Promise.all([ + collectAsyncDefinitions(this.datasources), + collectAsyncDefinitions(this.visualizations), + ]); return { capabilities: coreStart.application.capabilities, savedObjectsClient: coreStart.savedObjects.client, @@ -76,6 +82,8 @@ export class EditorFrameService { expressionRenderer: deps.expressions.ReactExpressionRenderer, indexPatternService: deps.data.indexPatterns, uiActions: deps.uiActions, + datasources: resolvedDatasources, + visualizations: resolvedVisualizations, }; }; @@ -88,7 +96,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/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index 687f7b413bf4d..de27defee5b59 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -192,12 +192,7 @@ export function getIndexPatternDatasource({ getMetaData(state: IndexPatternPrivateState) { return { filterableIndexPatterns: _.uniq( - Object.values(state.layers) - .map((layer) => layer.indexPatternId) - .map((indexPatternId) => ({ - id: indexPatternId, - title: state.indexPatterns[indexPatternId].title, - })) + Object.values(state.layers).map((layer) => layer.indexPatternId) ), }; }, 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..6cef58125eac3 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( 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..f232faa2bc0b8 100644 --- a/x-pack/plugins/lens/public/metric_visualization/metric_visualization.tsx +++ b/x-pack/plugins/lens/public/metric_visualization/metric_visualization.tsx @@ -8,7 +8,7 @@ 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 { State } from './types'; import chartMetricSVG from '../assets/chart_metric.svg'; const toExpression = ( @@ -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: [ 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.ts b/x-pack/plugins/lens/public/persistence/filter_references.ts index e81133b455e22..e6e421060e5b9 100644 --- a/x-pack/plugins/lens/public/persistence/filter_references.ts +++ b/x-pack/plugins/lens/public/persistence/filter_references.ts @@ -7,6 +7,7 @@ import { Filter } from 'src/plugins/data/public'; import { SavedObjectReference } from 'kibana/public'; import { FilterMeta } from 'src/plugins/data/common'; +import { Document } from './saved_object_store'; export interface PersistableFilterMeta extends FilterMeta { indexRefName?: string; @@ -16,6 +17,20 @@ export interface PersistableFilter extends Filter { meta: PersistableFilterMeta; } +export function getFilterableIndexPatternIds(doc: Document) { + return new Array(doc.state.datasourceMetaData.numberFilterableIndexPatterns) + .fill(undefined) + .map((_, index) => { + return doc.references.find(({ name }) => name === `filterable-index-pattern-${index}`)!.id; + }); +} + +export function filterableIndexPatternIdsToReferences(filterableIndexPatternIds: string[]) { + return filterableIndexPatternIds.map((id, index) => { + return { type: 'index-pattern', id, name: `filterable-index-pattern-${index}` }; + }); +} + export function extractFilterReferences( filters: Filter[] ): { persistableFilters: PersistableFilter[]; references: SavedObjectReference[] } { 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 12c05c8dee046..f4cffacbfba05 100644 --- a/x-pack/plugins/lens/public/persistence/saved_object_store.ts +++ b/x-pack/plugins/lens/public/persistence/saved_object_store.ts @@ -20,7 +20,7 @@ export interface Document { description?: string; state: { datasourceMetaData: { - filterableIndexPatterns: Array<{ id: string }>; + numberFilterableIndexPatterns: number; }; datasourceStates: Record; visualization: unknown; 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 369ab28293fbc..5367dd541fa2e 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/types.ts b/x-pack/plugins/lens/public/types.ts index 79122da0f0503..bffb663a1a63a 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -59,9 +59,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 { @@ -134,7 +132,7 @@ export interface DatasourceSuggestion { } export interface DatasourceMetaData { - filterableIndexPatterns: Array<{ id: string; title: string }>; + filterableIndexPatterns: string[]; } export type StateSetter = (newState: T | ((prevState: T) => T)) => void; @@ -410,7 +408,7 @@ export interface VisualizationType { label: string; } -export interface Visualization { +export interface Visualization { /** Plugin ID, such as "lnsXY" */ id: string; @@ -420,15 +418,7 @@ export interface Visualization { * - Loadingn from a saved visualization * - When using suggestions, the suggested state is passed in */ - initialize: ( - frame: FramePublicAPI, - state?: P, - savedObjectReferences?: SavedObjectReference[] - ) => T; - /** - * Can remove any state that should not be persisted to saved object, such as UI state - */ - getPersistableState: (state: T) => { state: P; savedObjectReferences: SavedObjectReference[] }; + initialize: (frame: FramePublicAPI, state?: T) => T; /** * Visualizations must provide at least one type for the chart switcher, diff --git a/x-pack/plugins/lens/public/xy_visualization/types.ts b/x-pack/plugins/lens/public/xy_visualization/types.ts index 605119535d1f0..5097762fd8588 100644 --- a/x-pack/plugins/lens/public/xy_visualization/types.ts +++ b/x-pack/plugins/lens/public/xy_visualization/types.ts @@ -254,7 +254,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 { From 2f1c7cd94ba6343c3d36bfc26e9fb7e33cc35ac6 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Thu, 13 Aug 2020 11:53:38 +0200 Subject: [PATCH 07/16] fix types and tests --- x-pack/plugins/lens/common/types.ts | 10 + .../lens/public/app_plugin/app.test.tsx | 41 ++- .../datatable_visualization/visualization.tsx | 4 +- .../editor_frame/config_panel/layer_panel.tsx | 1 - .../editor_frame/editor_frame.test.tsx | 20 +- .../editor_frame/editor_frame.tsx | 1 - .../editor_frame/expression_helpers.ts | 26 +- .../editor_frame/save.test.ts | 16 +- .../editor_frame/state_management.test.ts | 8 +- .../editor_frame/suggestion_helpers.ts | 2 +- .../editor_frame/suggestion_panel.tsx | 3 +- .../workspace_panel/chart_switch.tsx | 2 +- .../embeddable/embeddable.test.tsx | 12 +- .../embeddable/embeddable_factory.ts | 14 +- .../public/editor_frame_service/mocks.tsx | 1 - .../public/editor_frame_service/service.tsx | 2 - .../__mocks__/loader.ts | 4 + .../indexpattern.test.ts | 81 +++--- .../indexpattern_datasource/loader.test.ts | 11 +- .../metric_visualization.test.ts | 3 +- .../metric_visualization.tsx | 10 +- .../public/persistence/filter_references.ts | 10 +- .../persistence/saved_object_store.test.ts | 8 +- .../public/persistence/saved_object_store.ts | 2 +- .../public/pie_visualization/to_expression.ts | 20 +- x-pack/plugins/lens/public/types.ts | 11 +- .../xy_visualization/to_expression.test.ts | 10 +- .../public/xy_visualization/to_expression.ts | 41 +-- .../__snapshots__/migrations.test.ts.snap | 201 +++++++++++++++ x-pack/plugins/lens/server/migrations.test.ts | 240 ++++++++++++++++++ x-pack/plugins/lens/server/migrations.ts | 153 ++++++++++- 31 files changed, 804 insertions(+), 164 deletions(-) create mode 100644 x-pack/plugins/lens/server/__snapshots__/migrations.test.ts.snap 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..4c522c712b52a 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.test.tsx @@ -474,6 +474,7 @@ describe('Lens App', () => { onChange({ filterableIndexPatterns: [], doc: { id: initialDocId, ...lastKnownDoc } as Document, + isSaveable: true, }) ); @@ -507,7 +508,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 +528,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(); @@ -605,7 +608,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, }) ); @@ -683,7 +687,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 +727,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(); @@ -790,8 +796,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 +815,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, }); }); @@ -1080,7 +1088,8 @@ describe('Lens App', () => { act(() => onChange({ filterableIndexPatterns: [], - doc: ({ id: undefined, expression: 'valid expression' } as unknown) as Document, + doc: ({ id: undefined } as unknown) as Document, + isSaveable: true, }) ); instance.update(); @@ -1101,7 +1110,8 @@ describe('Lens App', () => { act(() => onChange({ filterableIndexPatterns: [], - doc: ({ id: undefined, expression: 'valid expression' } as unknown) as Document, + doc: ({ id: undefined } as unknown) as Document, + isSaveable: true, }) ); instance.update(); @@ -1125,7 +1135,8 @@ describe('Lens App', () => { act(() => onChange({ filterableIndexPatterns: [], - doc: ({ id: '1234', expression: 'different expression' } as unknown) as Document, + doc: ({ id: '1234' } as unknown) as Document, + isSaveable: true, }) ); instance.update(); @@ -1149,7 +1160,8 @@ describe('Lens App', () => { act(() => onChange({ filterableIndexPatterns: [], - doc: ({ id: '1234', expression: 'valid expression' } as unknown) as Document, + doc: ({ id: '1234' } as unknown) as Document, + isSaveable: true, }) ); instance.update(); @@ -1173,7 +1185,8 @@ describe('Lens App', () => { act(() => onChange({ filterableIndexPatterns: [], - doc: ({ id: '1234', expression: null } as unknown) as Document, + doc: ({ id: '1234' } as unknown) as Document, + isSaveable: true, }) ); instance.update(); diff --git a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx index dd721a7c0d32c..31c391a69e9d4 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx @@ -180,9 +180,9 @@ export const datatableVisualization: Visualization }; }, - toExpression(state, frame) { + toExpression(state, datasourceLayers) { const layer = state.layers[0]; - const datasource = frame.datasourceLayers[layer.layerId]; + const datasource = datasourceLayers[layer.layerId]; const operations = layer.columns .map((columnId) => ({ columnId, operation: datasource.getOperationForColumnId(columnId) })) .filter((o): o is { columnId: string; operation: Operation } => !!o.operation); 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 a384e339e8fbd..670397b76af23 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 @@ -123,7 +123,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..6aa7f5be5406c 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,7 +170,6 @@ describe('editor_frame', () => { doc={{ visualizationType: 'testVis', title: '', - expression: '', state: { datasourceStates: { testDatasource: datasource1State, @@ -178,11 +177,12 @@ describe('editor_frame', () => { }, visualization: {}, datasourceMetaData: { - filterableIndexPatterns: [], + numberFilterableIndexPatterns: 0, }, query: { query: '', language: 'lucene' }, filters: [], }, + references: [], }} /> ); @@ -499,7 +499,6 @@ describe('editor_frame', () => { doc={{ visualizationType: 'testVis', title: '', - expression: '', state: { datasourceStates: { testDatasource: {}, @@ -507,11 +506,12 @@ describe('editor_frame', () => { }, visualization: {}, datasourceMetaData: { - filterableIndexPatterns: [], + numberFilterableIndexPatterns: 0, }, query: { query: '', language: 'lucene' }, filters: [], }, + references: [], }} /> ); @@ -747,7 +747,6 @@ describe('editor_frame', () => { doc={{ visualizationType: 'testVis', title: '', - expression: '', state: { datasourceStates: { testDatasource: {}, @@ -755,11 +754,12 @@ describe('editor_frame', () => { }, visualization: {}, datasourceMetaData: { - filterableIndexPatterns: [], + numberFilterableIndexPatterns: 0, }, query: { query: '', language: 'lucene' }, filters: [], }, + references: [], }} /> ); @@ -802,7 +802,6 @@ describe('editor_frame', () => { doc={{ visualizationType: 'testVis', title: '', - expression: '', state: { datasourceStates: { testDatasource: datasource1State, @@ -810,11 +809,12 @@ describe('editor_frame', () => { }, visualization: {}, datasourceMetaData: { - filterableIndexPatterns: [], + numberFilterableIndexPatterns: 0, }, query: { query: '', language: 'lucene' }, filters: [], }, + references: [], }} /> ); @@ -1461,7 +1461,7 @@ describe('editor_frame', () => { ); mockDatasource.getLayers.mockReturnValue(['first']); mockDatasource.getMetaData.mockReturnValue({ - filterableIndexPatterns: [{ id: '1', title: 'resolved' }], + filterableIndexPatterns: ['1'], }); mockVisualization.initialize.mockReturnValue({ initialState: true }); @@ -1584,7 +1584,7 @@ describe('editor_frame', () => { mockDatasource.initialize.mockResolvedValue({}); mockDatasource.getLayers.mockReturnValue(['first']); mockDatasource.getMetaData.mockReturnValue({ - filterableIndexPatterns: [{ id: '1', title: 'resolved' }], + filterableIndexPatterns: ['1'], }); mockVisualization.initialize.mockReturnValue({ initialState: true }); 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 089c4f68d79b0..96dc0ca4215f4 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 @@ -111,7 +111,6 @@ export function EditorFrame(props: EditorFrameProps) { datasourceLayers[layer] = props.datasourceMap[id].getPublicAPI({ state: datasourceState, layerId: layer, - dateRange: props.dateRange, }); }); }); 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..68f112c5668aa 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,7 +5,8 @@ */ import { Ast, fromExpression, ExpressionFunctionAST } from '@kbn/interpreter/common'; -import { Visualization, Datasource, FramePublicAPI } from '../../types'; +import { DateRange } from '../../../common'; +import { Visualization, Datasource, DatasourcePublicAPI } from '../../types'; import { Filter, TimeRange, Query } from '../../../../../../src/plugins/data/public'; export function prependDatasourceExpression( @@ -114,22 +115,33 @@ export function buildExpression({ state: unknown; } >; - framePublicAPI: FramePublicAPI; + framePublicAPI: { + datasourceLayers: Record; + query: Query; + dateRange?: DateRange; + filters: Filter[]; + }; + removeDateRange?: boolean; }): Ast | null { if (visualization === null) { return null; } - const visualizationExpression = visualization.toExpression(visualizationState, framePublicAPI); + const visualizationExpression = visualization.toExpression( + visualizationState, + framePublicAPI.datasourceLayers + ); const expressionContext = removeDateRange ? { query: framePublicAPI.query, filters: framePublicAPI.filters } : { query: framePublicAPI.query, - timeRange: { - from: framePublicAPI.dateRange.fromDate, - to: framePublicAPI.dateRange.toDate, - }, + timeRange: framePublicAPI.dateRange + ? { + from: framePublicAPI.dateRange.fromDate, + to: framePublicAPI.dateRange.toDate, + } + : undefined, filters: framePublicAPI.filters, }; 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..c7629bd646bec 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 @@ -10,12 +10,14 @@ import { esFilters, IIndexPattern, IFieldType } from '../../../../../../src/plug 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,13 +49,13 @@ 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: [], })); const visualization = createMockVisualization(); - visualization.getPersistableState.mockImplementation((state) => ({ - things: `${state}_vis_persisted`, - })); const doc = await getSavedObjectFormat({ ...saveArgs, @@ -86,7 +88,7 @@ describe('save editor frame state', () => { stuff: '2_datasource_persisted', }, }, - visualization: { things: '4_vis_persisted' }, + visualization: '4', query: { query: '', language: 'lucene' }, filters: [ { 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..2f6dad10c28c8 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,7 +57,6 @@ describe('editor_frame state management', () => { const initialState = getInitialState({ ...props, doc: { - expression: '', state: { datasourceStates: { testDatasource: { internalState1: '' }, @@ -65,11 +64,12 @@ describe('editor_frame state management', () => { }, visualization: {}, datasourceMetaData: { - filterableIndexPatterns: [], + numberFilterableIndexPatterns: 0, }, query: { query: '', language: 'lucene' }, filters: [], }, + references: [], title: '', visualizationType: 'testVis', }, @@ -380,9 +380,8 @@ describe('editor_frame state management', () => { type: 'VISUALIZATION_LOADED', doc: { id: 'b', - expression: '', state: { - datasourceMetaData: { filterableIndexPatterns: [] }, + datasourceMetaData: { numberFilterableIndexPatterns: 0 }, datasourceStates: { a: { foo: 'c' } }, visualization: { bar: 'd' }, query: { query: '', language: 'lucene' }, @@ -392,6 +391,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.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx index aba8b70945129..346848752cc7e 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 @@ -389,7 +389,6 @@ function getPreviewExpression( if (updatedLayerApis[layerId]) { updatedLayerApis[layerId] = datasource.getPublicAPI({ layerId, - dateRange: frame.dateRange, state: datasourceState, }); } @@ -398,7 +397,7 @@ function getPreviewExpression( return visualization.toPreviewExpression( visualizableState.visualizationState, - suggestionFrameApi + suggestionFrameApi.datasourceLayers ); } diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx index 88b791a7875ef..5640c52ac4325 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx @@ -260,7 +260,7 @@ export function ChartSwitch(props: Props) { function getTopSuggestion( props: Props, visualizationId: string, - newVisualization: Visualization, + newVisualization: Visualization, subVisualizationId?: string ): Suggestion | undefined { const unfilteredSuggestions = getSuggestions({ 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..844ea6179c461 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,16 @@ jest.mock('../../../../../../src/plugins/inspector/public/', () => ({ })); const savedVis: Document = { - expression: 'my | expression', state: { visualization: {}, datasourceStates: {}, datasourceMetaData: { - filterableIndexPatterns: [], + numberFilterableIndexPatterns: 0, }, query: { query: '', language: 'lucene' }, filters: [], }, + references: [], title: 'My title', visualizationType: '', }; @@ -59,13 +59,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 +83,7 @@ describe('embeddable', () => { editUrl: '', editable: true, savedVis, + expression: 'my | expression', }, { id: '123' } ); @@ -110,6 +112,7 @@ describe('embeddable', () => { editUrl: '', editable: true, savedVis, + expression: 'my | expression', }, { id: '123', timeRange, query, filters } ); @@ -132,6 +135,7 @@ describe('embeddable', () => { editUrl: '', editable: true, savedVis, + expression: 'my | expression', }, { id: '123' } ); @@ -162,6 +166,7 @@ describe('embeddable', () => { editUrl: '', editable: true, savedVis, + expression: 'my | expression', }, { id: '123', timeRange, query, filters } ); @@ -195,6 +200,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_factory.ts b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts index 1de24d76c957e..ff2a63368049c 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts @@ -25,7 +25,7 @@ import { SavedObjectIndexStore, DOC_TYPE, getFilterableIndexPatternIds } from '. import { getEditPath } from '../../../common'; import { UiActionsStart } from '../../../../../../src/plugins/ui_actions/public'; import { buildExpression } from '../editor_frame/expression_helpers'; -import { Datasource, Visualization, DatasourcePublicAPI, FramePublicAPI } from '../../types'; +import { Datasource, Visualization, DatasourcePublicAPI } from '../../types'; interface StartServices { timefilter: TimefilterContract; @@ -126,24 +126,14 @@ export class EmbeddableFactory implements EmbeddableFactoryDefinition { datasourceLayers[layer] = datasource.getPublicAPI({ state: datasourceState, layerId: layer, - dateRange: { fromDate: 'now', toDate: 'now' }, }); }); }); - const framePublicAPI: FramePublicAPI = { + const framePublicAPI = { datasourceLayers, - dateRange: { fromDate: 'now', toDate: 'now' }, query: savedVis.state.query, filters: savedVis.state.filters, - - addNewLayer() { - throw new Error('adding new layer is not allowed in embedded mode'); - }, - - removeLayers() { - throw new Error('removing layers is not allowed in embedded mode'); - }, }; const expression = buildExpression({ 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..2be9863804334 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) => ({ 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 f66d6bc1ede2a..05bf23860f516 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/service.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/service.tsx @@ -8,8 +8,6 @@ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { I18nProvider } from '@kbn/i18n/react'; import { CoreSetup, CoreStart } from 'kibana/public'; -import { Observable, Subject } from 'rxjs'; -import { first } from 'rxjs/operators'; import { ExpressionsSetup, ExpressionsStart } from '../../../../../src/plugins/expressions/public'; import { EmbeddableSetup, EmbeddableStart } from '../../../../../src/plugins/embeddable/public'; import { 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 3bd0685551a4c..6012bb2f83af7 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts @@ -119,12 +119,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: {}, @@ -133,7 +136,10 @@ function stateFromPersistedState( } describe('IndexPattern Data Source', () => { - let persistedState: IndexPatternPersistedState; + let baseState: Omit< + IndexPatternPrivateState, + 'indexPatternRefs' | 'indexPatterns' | 'existingFields' | 'isFirstExistenceFetch' + >; let indexPatternDatasource: Datasource; beforeEach(() => { @@ -144,7 +150,7 @@ describe('IndexPattern Data Source', () => { charts: chartPluginMock.createSetupContract(), }); - persistedState = { + baseState = { currentIndexPatternId: '1', layers: { first: { @@ -215,9 +221,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' }, + ], + }); }); }); @@ -228,7 +262,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: { @@ -257,7 +291,7 @@ describe('IndexPattern Data Source', () => { }, }; - const state = stateFromPersistedState(queryPersistedState); + const state = enrichBaseState(queryBaseState); expect(indexPatternDatasource.toExpression(state, 'first')).toMatchInlineSnapshot(` Object { @@ -302,7 +336,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: { @@ -341,14 +375,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: { @@ -377,7 +411,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']); @@ -503,16 +537,7 @@ describe('IndexPattern Data Source', () => { currentIndexPatternId: '1', }) ).toEqual({ - filterableIndexPatterns: [ - { - id: '1', - title: 'my-fake-index-pattern', - }, - { - id: '2', - title: 'my-fake-restricted-pattern', - }, - ], + filterableIndexPatterns: ['1', '2'], }); }); }); @@ -521,14 +546,10 @@ describe('IndexPattern Data Source', () => { 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/loader.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts index 27904a0f23f16..6157b24d2410d 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts @@ -368,10 +368,8 @@ describe('loader', () => { it('should initialize from saved state', async () => { const savedState: IndexPatternPersistedState = { - currentIndexPatternId: 'b', layers: { layerb: { - indexPatternId: 'b', columnOrder: ['col1', 'col2'], columns: { col1: { @@ -397,7 +395,12 @@ describe('loader', () => { }; const storage = createMockStorage({ indexPatternId: 'a' }); const state = await loadInitialState({ - state: savedState, + persistedState: savedState, + references: [ + { name: 'indexpattern-datasource-current-indexpattern', id: 'b', type: 'index-pattern' }, + { name: 'indexpattern-datasource-layer-layerb', id: 'b', type: 'index-pattern' }, + { name: 'another-reference', id: 'c', type: 'index-pattern' }, + ], savedObjectsClient: mockClient(), storage, }); @@ -411,7 +414,7 @@ describe('loader', () => { indexPatterns: { b: sampleIndexPatterns.b, }, - layers: savedState.layers, + layers: { ...savedState.layers, indexPatternId: 'b' }, }); expect(storage.set).toHaveBeenCalledWith('lens-settings', { 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 6cef58125eac3..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 @@ -162,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 f232faa2bc0b8..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 { 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 { @@ -104,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/persistence/filter_references.ts b/x-pack/plugins/lens/public/persistence/filter_references.ts index e6e421060e5b9..7353435eb245b 100644 --- a/x-pack/plugins/lens/public/persistence/filter_references.ts +++ b/x-pack/plugins/lens/public/persistence/filter_references.ts @@ -6,16 +6,8 @@ import { Filter } from 'src/plugins/data/public'; import { SavedObjectReference } from 'kibana/public'; -import { FilterMeta } from 'src/plugins/data/common'; import { Document } from './saved_object_store'; - -export interface PersistableFilterMeta extends FilterMeta { - indexRefName?: string; -} - -export interface PersistableFilter extends Filter { - meta: PersistableFilterMeta; -} +import { PersistableFilter } from '../../common'; export function getFilterableIndexPatternIds(doc: Document) { return new Array(doc.state.datasourceMetaData.numberFilterableIndexPatterns) 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..18cd341dcf0c9 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,10 +30,10 @@ describe('LensStore', () => { title: 'Hello', description: 'My doc', visualizationType: 'bar', - expression: '', + references: [], state: { datasourceMetaData: { - filterableIndexPatterns: [], + numberFilterableIndexPatterns: 0, }, datasourceStates: { indexpattern: { type: 'index_pattern', indexPattern: '.kibana_test' }, @@ -87,9 +87,9 @@ describe('LensStore', () => { id: 'Gandalf', title: 'Even the very wise cannot see all ends.', visualizationType: 'line', - expression: '', + references: [], state: { - datasourceMetaData: { filterableIndexPatterns: [] }, + datasourceMetaData: { numberFilterableIndexPatterns: 0 }, 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 33138a32620ef..761efcec4b1fd 100644 --- a/x-pack/plugins/lens/public/persistence/saved_object_store.ts +++ b/x-pack/plugins/lens/public/persistence/saved_object_store.ts @@ -9,8 +9,8 @@ import { SavedObjectsClientContract, SavedObjectReference, } from 'kibana/public'; -import { PersistableFilter } from './filter_references'; import { Query } from '../../../../../src/plugins/data/public'; +import { PersistableFilter } from '../../common'; export interface Document { id?: string; 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 bffb663a1a63a..4feb6fd01f825 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -31,7 +31,6 @@ export type FormatFactory = (mapping?: SerializedFieldFormat) => IFieldFormat; export interface PublicAPIProps { state: T; layerId: string; - dateRange: DateRange; } export interface EditorFrameProps { @@ -500,12 +499,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 31b34e41e82db..5a09e5ffee9eb 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 @@ -51,7 +51,7 @@ describe('#toExpression', () => { }, ], }, - frame + frame.datasourceLayers ) ).toMatchSnapshot(); }); @@ -72,7 +72,7 @@ describe('#toExpression', () => { }, ], }, - frame + frame.datasourceLayers ) as Ast).chain[0].arguments.fittingFunction[0] ).toEqual('None'); }); @@ -93,7 +93,7 @@ describe('#toExpression', () => { }, ], }, - frame + frame.datasourceLayers ) ).toBeNull(); }); @@ -114,7 +114,7 @@ describe('#toExpression', () => { }, ], }, - frame + frame.datasourceLayers ) ).toBeNull(); }); @@ -134,7 +134,7 @@ describe('#toExpression', () => { }, ], }, - frame + frame.datasourceLayers )! as Ast; expect(mockDatasource.publicAPIMock.getOperationForColumnId).toHaveBeenCalledWith('b'); 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 b17704b38cdec..45cda54a94aa0 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,13 @@ 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; } -function xyTitles(layer: LayerConfig, frame: FramePublicAPI) { +function xyTitles(layer: LayerConfig, datasourceLayers: Record) { const defaults = { xTitle: 'x', yTitle: 'y', @@ -22,7 +22,7 @@ function xyTitles(layer: LayerConfig, frame: FramePublicAPI) { if (!layer || !layer.accessors.length) { return defaults; } - const datasource = frame.datasourceLayers[layer.layerId]; + const datasource = datasourceLayers[layer.layerId]; if (!datasource) { return defaults; } @@ -35,7 +35,10 @@ function xyTitles(layer: LayerConfig, frame: FramePublicAPI) { }; } -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; } @@ -43,19 +46,25 @@ 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, xyTitles(state.layers[0], frame)); + return buildExpression( + state, + metadata, + datasourceLayers, + xyTitles(state.layers[0], datasourceLayers) + ); }; -export function toPreviewExpression(state: State, frame: FramePublicAPI) { +export function toPreviewExpression( + state: State, + datasourceLayers: Record +) { return toExpression( { ...state, @@ -66,7 +75,7 @@ export function toPreviewExpression(state: State, frame: FramePublicAPI) { isVisible: false, }, }, - frame + datasourceLayers ); } @@ -99,7 +108,7 @@ export function getScaleType(metadata: OperationMetadata | null, defaultScale: S export const buildExpression = ( state: State, metadata: Record>, - frame?: FramePublicAPI, + datasourceLayers?: Record, { xTitle, yTitle }: { xTitle: string; yTitle: string } = { xTitle: '', yTitle: '' } ): Ast | null => { const validLayers = state.layers.filter((layer): layer is ValidLayer => @@ -140,8 +149,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) => { @@ -153,8 +162,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/server/__snapshots__/migrations.test.ts.snap b/x-pack/plugins/lens/server/__snapshots__/migrations.test.ts.snap new file mode 100644 index 0000000000000..2c70f2d1ca9ef --- /dev/null +++ b/x-pack/plugins/lens/server/__snapshots__/migrations.test.ts.snap @@ -0,0 +1,201 @@ +// 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 { + "datasourceMetaData": Object { + "numberFilterableIndexPatterns": 2, + }, + "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": "filterable-index-pattern-0", + "type": "index-pattern", + }, + Object { + "id": "ff959d40-b880-11e8-a6d9-e546fe2bba5f", + "name": "filterable-index-pattern-1", + "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..ce1a4160a0cca 100644 --- a/x-pack/plugins/lens/server/migrations.test.ts +++ b/x-pack/plugins/lens/server/migrations.test.ts @@ -278,4 +278,244 @@ 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 list references for filterable index pattern ids', () => { + const result = migrations['7.10.0'](example, context); + expect( + result.references?.find((ref) => ref.name === 'filterable-index-pattern-0')?.id + ).toEqual('90943e30-9a47-11e8-b64d-95841ca0b247'); + expect( + result.references?.find((ref) => ref.name === 'filterable-index-pattern-1')?.id + ).toEqual('ff959d40-b880-11e8-a6d9-e546fe2bba5f'); + }); + + it('should remove filterable index patterns', () => { + const result = migrations['7.10.0'](example, context); + expect(result.attributes.state.datasourceMetaData.filterableIndexPatterns).toBeUndefined(); + expect(result.attributes.state.datasourceMetaData.numberFilterableIndexPatterns).toEqual(2); + }); + + 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..9db622d49c062 100644 --- a/x-pack/plugins/lens/server/migrations.ts +++ b/x-pack/plugins/lens/server/migrations.ts @@ -4,19 +4,53 @@ * you may not use this file except in compliance with the Elastic License. */ -import { cloneDeep } from 'lodash'; +import { cloneDeep, uniqBy } 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 LensDocShapePre710 { + visualizationType: string | null; + title: string; + expression: string | null; + state: { + datasourceMetaData: { + filterableIndexPatterns: Array<{ id: string; title: string }>; + }; + datasourceStates: { + // This is hardcoded as our only datasource + indexpattern: { + currentIndexPatternId: string; + layers: Record< + string, + { + columnOrder: string[]; + columns: Record; + indexPatternId: string; + } + >; + }; + }; + visualization: VisualizationState; + query: Query; + filters: Filter[]; + }; +} interface LensDocShape { id?: string; type?: string; visualizationType: string | null; title: string; - expression: string | null; state: { datasourceMetaData: { - filterableIndexPatterns: Array<{ id: string; title: string }>; + numberFilterableIndexPatterns: number; }; datasourceStates: { // This is hardcoded as our only datasource @@ -31,8 +65,8 @@ interface LensDocShape { }; }; visualization: VisualizationState; - query: unknown; - filters: unknown[]; + query: Query; + filters: PersistableFilter[]; }; } @@ -55,7 +89,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 +149,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 +214,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 +237,100 @@ 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 unique filterable index patterns to reference list + const uniqueFilterableIndexPatterns = uniqBy( + attributes.state.datasourceMetaData.filterableIndexPatterns, + 'id' + ); + savedObjectReferences.push( + ...uniqueFilterableIndexPatterns.map(({ id }, index) => { + return { type: 'index-pattern', id, name: `filterable-index-pattern-${index}` }; + }) + ); + + // 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: { + datasourceMetaData: { + numberFilterableIndexPatterns: uniqueFilterableIndexPatterns.length, + }, + 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, }; From 476b6d207ca676a36257f8cf4c85991f5ff4383b Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Thu, 13 Aug 2020 17:20:47 +0200 Subject: [PATCH 08/16] fix tests --- .../lens/public/app_plugin/app.test.tsx | 94 +++++++++++-------- .../__mocks__/expression_helpers.ts | 14 +++ .../editor_frame/editor_frame.test.tsx | 48 ++++++---- .../editor_frame/save.test.ts | 20 +++- .../public/editor_frame_service/mocks.tsx | 2 +- .../indexpattern_datasource/loader.test.ts | 2 +- .../public/persistence/filter_references.ts | 5 +- .../persistence/saved_object_store.test.ts | 47 ++++++---- 8 files changed, 148 insertions(+), 84 deletions(-) create mode 100644 x-pack/plugins/lens/public/editor_frame_service/editor_frame/__mocks__/expression_helpers.ts 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 4c522c712b52a..df5debc62e3f1 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,10 +282,10 @@ 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: [], + datasourceMetaData: { numberFilterableIndexPatterns: 0 }, }, }); await act(async () => { @@ -312,12 +312,12 @@ 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' }] }, + datasourceMetaData: { numberFilterableIndexPatterns: 1 }, }, + references: [{ type: 'index-pattern', id: '1', name: 'filterable-index-pattern-0' }], }); instance = mount(); @@ -341,15 +341,14 @@ 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' }] }, - }, - }, + datasourceMetaData: { numberFilterableIndexPatterns: 1 }, + }), + }), }) ); }); @@ -433,7 +432,15 @@ describe('Lens App', () => { } async function save({ - lastKnownDoc = { expression: 'kibana 3' }, + lastKnownDoc = { + references: [], + state: { + filters: [], + datasourceMetaData: { + numberFilterableIndexPatterns: 0, + }, + }, + }, initialDocId, ...saveProps }: SaveProps & { @@ -447,16 +454,15 @@ 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' }] }, + datasourceMetaData: { numberFilterableIndexPatterns: 0 }, filters: [], }, }); (args.docStorage.save as jest.Mock).mockImplementation(async ({ id }) => ({ id: id || 'aaa', - expression: 'kibana 2', })); await act(async () => { @@ -544,11 +550,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); @@ -564,11 +571,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); @@ -584,11 +592,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(); @@ -633,11 +642,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); }); @@ -1052,10 +1062,10 @@ 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: [], + datasourceMetaData: { numberFilterableIndexPatterns: 0 }, }, } as jest.ResolvedValue); }); @@ -1160,7 +1170,15 @@ describe('Lens App', () => { act(() => onChange({ filterableIndexPatterns: [], - doc: ({ id: '1234' } as unknown) as Document, + doc: ({ + id: '1234', + title: 'My cool doc', + state: { + query: 'kuery', + filters: [], + datasourceMetaData: { numberFilterableIndexPatterns: 0 }, + }, + } as unknown) as Document, isSaveable: true, }) ); 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/editor_frame.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx index 6aa7f5be5406c..e57b07d0d77e6 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 @@ -187,8 +187,8 @@ describe('editor_frame', () => { /> ); }); - 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(); }); @@ -842,7 +842,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 +849,6 @@ describe('editor_frame', () => { mount( { }); expect(mockDatasource.getPublicAPI).toHaveBeenCalledWith({ - dateRange, state: datasourceState, layerId: 'first', }); @@ -1487,14 +1484,21 @@ 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: 'filterable-index-pattern-0', + type: 'index-pattern', + }, + ], state: { visualization: null, // Not yet loaded - datasourceMetaData: { filterableIndexPatterns: [{ id: '1', title: 'resolved' }] }, - datasourceStates: { testDatasource: undefined }, + datasourceMetaData: { numberFilterableIndexPatterns: 1 }, + datasourceStates: { testDatasource: {} }, query: { query: '', language: 'lucene' }, filters: [], }, @@ -1502,18 +1506,24 @@ 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: 'filterable-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 }, + datasourceMetaData: { numberFilterableIndexPatterns: 1 }, + datasourceStates: { testDatasource: {} }, query: { query: '', language: 'lucene' }, filters: [], }, @@ -1521,6 +1531,7 @@ describe('editor_frame', () => { type: 'lens', visualizationType: 'testVis', }, + isSaveable: false, }); }); @@ -1562,11 +1573,11 @@ describe('editor_frame', () => { expect(onChange).toHaveBeenNthCalledWith(3, { filterableIndexPatterns: [], doc: { - expression: expect.stringContaining('vis "expression"'), id: undefined, + references: [], state: { - datasourceMetaData: { filterableIndexPatterns: [] }, - datasourceStates: { testDatasource: undefined }, + datasourceMetaData: { numberFilterableIndexPatterns: 0 }, + datasourceStates: { testDatasource: { datasource: '' } }, visualization: { initialState: true }, query: { query: 'new query', language: 'lucene' }, filters: [], @@ -1575,6 +1586,7 @@ describe('editor_frame', () => { type: 'lens', visualizationType: 'testVis', }, + isSaveable: true, }); }); 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 c7629bd646bec..0bd02a81b7922 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,6 +8,8 @@ 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(); const mockDatasource = createMockDatasource('a'); @@ -54,10 +56,12 @@ describe('save editor frame state', () => { }, savedObjectReferences: [], })); + datasource.toExpression.mockReturnValue('my | expr'); const visualization = createMockVisualization(); + visualization.toExpression.mockReturnValue('vis | expr'); - const doc = await getSavedObjectFormat({ + const { doc, filterableIndexPatterns, isSaveable } = await getSavedObjectFormat({ ...saveArgs, activeDatasources: { indexpattern: datasource, @@ -76,12 +80,13 @@ describe('save editor frame state', () => { visualization, }); + expect(filterableIndexPatterns).toEqual([]); + expect(isSaveable).toEqual(true); expect(doc).toEqual({ id: undefined, - expression: '', state: { datasourceMetaData: { - filterableIndexPatterns: [], + numberFilterableIndexPatterns: 0, }, datasourceStates: { indexpattern: { @@ -92,11 +97,18 @@ describe('save editor frame state', () => { 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/mocks.tsx b/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx index 2be9863804334..98d8dc892abab 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx @@ -70,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(), 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 6157b24d2410d..fa525d25be774 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts @@ -414,7 +414,7 @@ describe('loader', () => { indexPatterns: { b: sampleIndexPatterns.b, }, - layers: { ...savedState.layers, indexPatternId: 'b' }, + layers: { layerb: { ...savedState.layers.layerb, indexPatternId: 'b' } }, }); expect(storage.set).toHaveBeenCalledWith('lens-settings', { diff --git a/x-pack/plugins/lens/public/persistence/filter_references.ts b/x-pack/plugins/lens/public/persistence/filter_references.ts index 7353435eb245b..5eb3e743ec42a 100644 --- a/x-pack/plugins/lens/public/persistence/filter_references.ts +++ b/x-pack/plugins/lens/public/persistence/filter_references.ts @@ -13,8 +13,9 @@ export function getFilterableIndexPatternIds(doc: Document) { return new Array(doc.state.datasourceMetaData.numberFilterableIndexPatterns) .fill(undefined) .map((_, index) => { - return doc.references.find(({ name }) => name === `filterable-index-pattern-${index}`)!.id; - }); + return doc.references.find(({ name }) => name === `filterable-index-pattern-${index}`)?.id; + }) + .filter(Boolean) as string[]; } export function filterableIndexPatternIdsToReferences(filterableIndexPatternIds: string[]) { 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 18cd341dcf0c9..8bd9538798364 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 @@ -49,10 +49,10 @@ describe('LensStore', () => { title: 'Hello', description: 'My doc', visualizationType: 'bar', - expression: '', + references: [], state: { datasourceMetaData: { - filterableIndexPatterns: [], + numberFilterableIndexPatterns: 0, }, datasourceStates: { indexpattern: { type: 'index_pattern', indexPattern: '.kibana_test' }, @@ -64,21 +64,28 @@ 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: { + datasourceMetaData: { + numberFilterableIndexPatterns: 0, + }, + 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 () => { @@ -101,9 +108,9 @@ describe('LensStore', () => { id: 'Gandalf', title: 'Even the very wise cannot see all ends.', visualizationType: 'line', - expression: '', + references: [], state: { - datasourceMetaData: { filterableIndexPatterns: [] }, + datasourceMetaData: { numberFilterableIndexPatterns: 0 }, datasourceStates: { indexpattern: { type: 'index_pattern', indexPattern: 'lotr' } }, visualization: { gear: ['staff', 'pointy hat'] }, query: { query: '', language: 'lucene' }, @@ -116,22 +123,22 @@ 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: [] }, + datasourceMetaData: { numberFilterableIndexPatterns: 0 }, datasourceStates: { indexpattern: { type: 'index_pattern', indexPattern: 'lotr' } }, visualization: { gear: ['staff', 'pointy hat'] }, query: { query: '', language: 'lucene' }, From e82c1faa39263abc15814c410ba4f307aa379656 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Fri, 14 Aug 2020 11:24:21 +0200 Subject: [PATCH 09/16] fix tests --- .../lens/public/xy_visualization/to_expression.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 90b17003ac069..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 @@ -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); @@ -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 From 660cfe8ad869b682c9f79db5bc391284fca797bc Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Fri, 14 Aug 2020 14:58:23 +0200 Subject: [PATCH 10/16] do not show modal on global filters --- .../lens/public/app_plugin/app.test.tsx | 2 +- x-pack/plugins/lens/public/app_plugin/app.tsx | 45 ++++++++++++------- 2 files changed, 30 insertions(+), 17 deletions(-) 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 df5debc62e3f1..90320885040a3 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.test.tsx @@ -1120,7 +1120,7 @@ describe('Lens App', () => { act(() => onChange({ filterableIndexPatterns: [], - doc: ({ id: undefined } as unknown) as Document, + doc: ({ id: undefined, state: {} } as unknown) as Document, isSaveable: true, }) ); diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx index 6c853a981cad2..e7f06a36c430e 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.tsx @@ -177,13 +177,33 @@ 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 && - !_.isEqual(state.persistedDoc, lastKnownDoc) + !_.isEqual(state.persistedDoc?.state, getLastKnownDocWithoutPinnedFilters()?.state) ) { return actions.confirm( i18n.translate('xpack.lens.app.unsavedWorkMessage', { @@ -197,7 +217,13 @@ export function App({ return actions.default(); } }); - }, [lastKnownDoc, onAppLeave, state.persistedDoc, core.application.capabilities.visualize.save]); + }, [ + lastKnownDoc, + onAppLeave, + state.persistedDoc, + core.application.capabilities.visualize.save, + getLastKnownDocWithoutPinnedFilters, + ]); // Sync Kibana breadcrumbs any time the saved document's title changes useEffect(() => { @@ -288,22 +314,9 @@ export function App({ if (!lastKnownDoc) { return; } - const [pinnedFilters, appFilters] = _.partition( - injectFilterReferences(lastKnownDoc.state?.filters || [], lastKnownDoc.references), - 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, From 451fb873311fc8c231514595ed6b9445c39684df Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Mon, 17 Aug 2020 10:07:19 +0200 Subject: [PATCH 11/16] fix on app leave condition --- x-pack/plugins/lens/public/app_plugin/app.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx index e7f06a36c430e..4f218b26a5a98 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.tsx @@ -203,7 +203,8 @@ export function App({ // or when the user has configured something without saving if ( core.application.capabilities.visualize.save && - !_.isEqual(state.persistedDoc?.state, getLastKnownDocWithoutPinnedFilters()?.state) + !_.isEqual(state.persistedDoc?.state, getLastKnownDocWithoutPinnedFilters()?.state) && + (state.isSaveable || state.persistedDoc) ) { return actions.confirm( i18n.translate('xpack.lens.app.unsavedWorkMessage', { @@ -221,6 +222,7 @@ export function App({ lastKnownDoc, onAppLeave, state.persistedDoc, + state.isSaveable, core.application.capabilities.visualize.save, getLastKnownDocWithoutPinnedFilters, ]); From 2e5d279bdeee4b9fc93054023c86b9d9937d4580 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Mon, 17 Aug 2020 11:01:48 +0200 Subject: [PATCH 12/16] fix datatable test --- .../public/datatable_visualization/visualization.test.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 ccc8529e9d2e6..194f12cf9291b 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx @@ -326,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); From fdf6061605e134dd167c9d5cb4c6d39f05d9ef03 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Thu, 20 Aug 2020 12:41:16 +0200 Subject: [PATCH 13/16] refactor --- .../lens/public/app_plugin/app.test.tsx | 13 +-- x-pack/plugins/lens/public/app_plugin/app.tsx | 11 +-- .../editor_frame/editor_frame.test.tsx | 33 ++----- .../editor_frame/editor_frame.tsx | 91 +++++++------------ .../editor_frame/expression_helpers.ts | 70 ++------------ .../editor_frame/save.test.ts | 3 - .../editor_frame_service/editor_frame/save.ts | 24 ++--- .../editor_frame/state_helpers.ts | 84 +++++++++++++++++ .../editor_frame/state_management.test.ts | 4 - .../editor_frame/suggestion_panel.tsx | 17 +++- .../workspace_panel/workspace_panel.tsx | 19 ++++ .../embeddable/embeddable.test.tsx | 3 - .../embeddable/embeddable.tsx | 46 +++++++--- .../embeddable/embeddable_factory.ts | 80 ++++------------ .../embeddable/expression_wrapper.tsx | 13 +-- .../public/editor_frame_service/mocks.tsx | 1 - .../public/editor_frame_service/service.tsx | 18 ++++ .../indexpattern.test.ts | 28 ------ .../indexpattern_datasource/indexpattern.tsx | 8 -- .../public/persistence/filter_references.ts | 16 ---- .../persistence/saved_object_store.test.ts | 12 --- .../public/persistence/saved_object_store.ts | 3 - x-pack/plugins/lens/public/types.ts | 8 +- .../__snapshots__/migrations.test.ts.snap | 13 --- x-pack/plugins/lens/server/migrations.test.ts | 15 +-- x-pack/plugins/lens/server/migrations.ts | 17 ---- 26 files changed, 256 insertions(+), 394 deletions(-) create mode 100644 x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts 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 90320885040a3..777e8b8ab78a3 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.test.tsx @@ -285,7 +285,6 @@ describe('Lens App', () => { state: { query: 'fake query', filters: [], - datasourceMetaData: { numberFilterableIndexPatterns: 0 }, }, }); await act(async () => { @@ -315,9 +314,8 @@ describe('Lens App', () => { state: { query: 'fake query', filters: [{ query: { match_phrase: { src: 'test' } } }], - datasourceMetaData: { numberFilterableIndexPatterns: 1 }, }, - references: [{ type: 'index-pattern', id: '1', name: 'filterable-index-pattern-0' }], + references: [{ type: 'index-pattern', id: '1', name: 'index-pattern-0' }], }); instance = mount(); @@ -346,7 +344,6 @@ describe('Lens App', () => { state: expect.objectContaining({ query: 'fake query', filters: [{ query: { match_phrase: { src: 'test' } } }], - datasourceMetaData: { numberFilterableIndexPatterns: 1 }, }), }), }) @@ -409,7 +406,6 @@ describe('Lens App', () => { expression: 'valid expression', state: { query: 'kuery', - datasourceMetaData: { filterableIndexPatterns: [{ id: '1', title: 'saved' }] }, }, } as jest.ResolvedValue); }); @@ -436,9 +432,6 @@ describe('Lens App', () => { references: [], state: { filters: [], - datasourceMetaData: { - numberFilterableIndexPatterns: 0, - }, }, }, initialDocId, @@ -457,7 +450,6 @@ describe('Lens App', () => { references: [], state: { query: 'fake query', - datasourceMetaData: { numberFilterableIndexPatterns: 0 }, filters: [], }, }); @@ -761,7 +753,6 @@ describe('Lens App', () => { expression: 'valid expression', state: { query: 'kuery', - datasourceMetaData: { filterableIndexPatterns: [{ id: '1', title: 'saved' }] }, }, } as jest.ResolvedValue); }); @@ -1065,7 +1056,6 @@ describe('Lens App', () => { state: { query: 'kuery', filters: [], - datasourceMetaData: { numberFilterableIndexPatterns: 0 }, }, } as jest.ResolvedValue); }); @@ -1176,7 +1166,6 @@ describe('Lens App', () => { state: { query: 'kuery', filters: [], - datasourceMetaData: { numberFilterableIndexPatterns: 0 }, }, } as unknown) as Document, isSaveable: true, diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx index 4f218b26a5a98..10701685ffff5 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.tsx @@ -27,12 +27,7 @@ import { OnSaveProps, checkForDuplicateTitle, } from '../../../../../src/plugins/saved_objects/public'; -import { - Document, - SavedObjectStore, - injectFilterReferences, - getFilterableIndexPatternIds, -} from '../persistence'; +import { Document, SavedObjectStore, injectFilterReferences } from '../persistence'; import { EditorFrameInstance } from '../types'; import { NativeRenderer } from '../native_renderer'; import { trackUiEvent } from '../lens_ui_telemetry'; @@ -256,7 +251,9 @@ export function App({ .load(docId) .then((doc) => { getAllIndexPatterns( - getFilterableIndexPatternIds(doc), + _.uniq( + doc.references.filter(({ type }) => type === 'index-pattern').map(({ id }) => id) + ), data.indexPatterns, core.notifications ) 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 e57b07d0d77e6..b7ebdb43bb747 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 @@ -176,9 +176,6 @@ describe('editor_frame', () => { testDatasource2: datasource2State, }, visualization: {}, - datasourceMetaData: { - numberFilterableIndexPatterns: 0, - }, query: { query: '', language: 'lucene' }, filters: [], }, @@ -505,9 +502,6 @@ describe('editor_frame', () => { testDatasource2: {}, }, visualization: {}, - datasourceMetaData: { - numberFilterableIndexPatterns: 0, - }, query: { query: '', language: 'lucene' }, filters: [], }, @@ -753,9 +747,6 @@ describe('editor_frame', () => { testDatasource2: {}, }, visualization: {}, - datasourceMetaData: { - numberFilterableIndexPatterns: 0, - }, query: { query: '', language: 'lucene' }, filters: [], }, @@ -808,9 +799,6 @@ describe('editor_frame', () => { testDatasource2: datasource2State, }, visualization: {}, - datasourceMetaData: { - numberFilterableIndexPatterns: 0, - }, query: { query: '', language: 'lucene' }, filters: [], }, @@ -1457,9 +1445,10 @@ describe('editor_frame', () => { }) ); mockDatasource.getLayers.mockReturnValue(['first']); - mockDatasource.getMetaData.mockReturnValue({ - filterableIndexPatterns: ['1'], - }); + mockDatasource.getPersistableState = jest.fn((x) => ({ + state: x, + savedObjectReferences: [{ type: 'index-pattern', id: '1', name: '' }], + })); mockVisualization.initialize.mockReturnValue({ initialState: true }); await act(async () => { @@ -1491,13 +1480,12 @@ describe('editor_frame', () => { references: [ { id: '1', - name: 'filterable-index-pattern-0', + name: 'index-pattern-0', type: 'index-pattern', }, ], state: { visualization: null, // Not yet loaded - datasourceMetaData: { numberFilterableIndexPatterns: 1 }, datasourceStates: { testDatasource: {} }, query: { query: '', language: 'lucene' }, filters: [], @@ -1514,7 +1502,7 @@ describe('editor_frame', () => { references: [ { id: '1', - name: 'filterable-index-pattern-0', + name: 'index-pattern-0', type: 'index-pattern', }, ], @@ -1522,7 +1510,6 @@ describe('editor_frame', () => { id: undefined, state: { visualization: { initialState: true }, // Now loaded - datasourceMetaData: { numberFilterableIndexPatterns: 1 }, datasourceStates: { testDatasource: {} }, query: { query: '', language: 'lucene' }, filters: [], @@ -1576,7 +1563,6 @@ describe('editor_frame', () => { id: undefined, references: [], state: { - datasourceMetaData: { numberFilterableIndexPatterns: 0 }, datasourceStates: { testDatasource: { datasource: '' } }, visualization: { initialState: true }, query: { query: 'new query', language: 'lucene' }, @@ -1595,9 +1581,10 @@ describe('editor_frame', () => { mockDatasource.initialize.mockResolvedValue({}); mockDatasource.getLayers.mockReturnValue(['first']); - mockDatasource.getMetaData.mockReturnValue({ - filterableIndexPatterns: ['1'], - }); + 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 96dc0ca4215f4..aedeb5de634d1 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, DatasourcePublicAPI, 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,7 +40,7 @@ export interface EditorFrameProps { filters: Filter[]; savedQuery?: SavedQuery; onChange: (arg: { - filterableIndexPatterns: DatasourceMetaData['filterableIndexPatterns']; + filterableIndexPatterns: string[]; doc: Document; isSaveable: boolean; }) => void; @@ -68,28 +63,20 @@ 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, - props.doc?.references - ) - .then((datasourceState) => { - if (!isUnmounted) { - dispatch({ - type: 'UPDATE_DATASOURCE_STATE', - updater: datasourceState, - datasourceId, - }); - } - }) - .catch(onError); - } - }); + initializeDatasources( + props.datasourceMap, + state.datasourceStates, + (datasourceId, datasourceState) => { + if (!isUnmounted) { + dispatch({ + type: 'UPDATE_DATASOURCE_STATE', + updater: datasourceState, + datasourceId, + }); + } + }, + onError + ); } return () => { isUnmounted = true; @@ -99,21 +86,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, - }); - }); - }); + const datasourceLayers = createDatasourceLayers(props.datasourceMap, state.datasourceStates); const framePublicAPI: FramePublicAPI = { datasourceLayers, @@ -220,20 +193,20 @@ export function EditorFrame(props: EditorFrameProps) { return; } - const { filterableIndexPatterns, doc, isSaveable } = getSavedObjectFormat({ - activeDatasources: Object.keys(state.datasourceStates).reduce( - (datasourceMap, datasourceId) => ({ - ...datasourceMap, - [datasourceId]: props.datasourceMap[datasourceId], - }), - {} - ), - visualization: activeVisualization, - state, - framePublicAPI, - }); - - props.onChange({ filterableIndexPatterns, doc, isSaveable }); + props.onChange( + getSavedObjectFormat({ + activeDatasources: Object.keys(state.datasourceStates).reduce( + (datasourceMap, datasourceId) => ({ + ...datasourceMap, + [datasourceId]: props.datasourceMap[datasourceId], + }), + {} + ), + visualization: activeVisualization, + state, + framePublicAPI, + }) + ); }, // 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 68f112c5668aa..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,9 +5,7 @@ */ import { Ast, fromExpression, ExpressionFunctionAST } from '@kbn/interpreter/common'; -import { DateRange } from '../../../common'; import { Visualization, Datasource, DatasourcePublicAPI } from '../../types'; -import { Filter, TimeRange, Query } from '../../../../../../src/plugins/data/public'; export function prependDatasourceExpression( visualizationExpression: Ast | string | null, @@ -59,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, ], }; } @@ -102,8 +72,7 @@ export function buildExpression({ visualizationState, datasourceMap, datasourceStates, - framePublicAPI, - removeDateRange, + datasourceLayers, }: { visualization: Visualization | null; visualizationState: unknown; @@ -115,35 +84,12 @@ export function buildExpression({ state: unknown; } >; - framePublicAPI: { - datasourceLayers: Record; - query: Query; - dateRange?: DateRange; - filters: Filter[]; - }; - - removeDateRange?: boolean; + datasourceLayers: Record; }): Ast | null { if (visualization === null) { return null; } - const visualizationExpression = visualization.toExpression( - visualizationState, - framePublicAPI.datasourceLayers - ); - - const expressionContext = removeDateRange - ? { query: framePublicAPI.query, filters: framePublicAPI.filters } - : { - query: framePublicAPI.query, - timeRange: framePublicAPI.dateRange - ? { - from: framePublicAPI.dateRange.fromDate, - to: framePublicAPI.dateRange.toDate, - } - : undefined, - filters: framePublicAPI.filters, - }; + const visualizationExpression = visualization.toExpression(visualizationState, datasourceLayers); const completeExpression = prependDatasourceExpression( visualizationExpression, @@ -151,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 0bd02a81b7922..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 @@ -85,9 +85,6 @@ describe('save editor frame state', () => { expect(doc).toEqual({ id: undefined, state: { - datasourceMetaData: { - numberFilterableIndexPatterns: 0, - }, datasourceStates: { indexpattern: { stuff: '2_datasource_persisted', 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 1cdce18ed003c..fe4a2b10d3fb5 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 @@ -8,8 +8,8 @@ import _ from 'lodash'; import { SavedObjectReference } from 'kibana/public'; import { EditorFrameState } from './state_management'; import { Document } from '../../persistence/saved_object_store'; -import { Datasource, Visualization, FramePublicAPI, DatasourceMetaData } from '../../types'; -import { extractFilterReferences, filterableIndexPatternIdsToReferences } from '../../persistence'; +import { Datasource, Visualization, FramePublicAPI } from '../../types'; +import { extractFilterReferences } from '../../persistence'; import { buildExpression } from './expression_helpers'; export interface Props { @@ -26,7 +26,7 @@ export function getSavedObjectFormat({ framePublicAPI, }: Props): { doc: Document; - filterableIndexPatterns: DatasourceMetaData['filterableIndexPatterns']; + filterableIndexPatterns: string[]; isSaveable: boolean; } { const datasourceStates: Record = {}; @@ -39,16 +39,9 @@ export function getSavedObjectFormat({ references.push(...savedObjectReferences); }); - const filterableIndexPatterns: string[] = []; - Object.entries(activeDatasources).forEach(([id, datasource]) => { - filterableIndexPatterns.push( - ...datasource.getMetaData(state.datasourceStates[id].state).filterableIndexPatterns - ); - }); - - const uniqueFilterableIndexPatternIds = _.uniq(filterableIndexPatterns); - - references.push(...filterableIndexPatternIdsToReferences(uniqueFilterableIndexPatternIds)); + const uniqueFilterableIndexPatternIds = _.uniq( + references.filter(({ type }) => type === 'index-pattern').map(({ id }) => id) + ); const { persistableFilters, references: filterReferences } = extractFilterReferences( framePublicAPI.filters @@ -74,16 +67,13 @@ export function getSavedObjectFormat({ visualizationType: state.visualization.activeId, state: { datasourceStates, - datasourceMetaData: { - numberFilterableIndexPatterns: uniqueFilterableIndexPatternIds.length, - }, visualization: state.visualization.state, query: framePublicAPI.query, filters: persistableFilters, }, references, }, - filterableIndexPatterns, + 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..655b44bca6cfe --- /dev/null +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts @@ -0,0 +1,84 @@ +/* + * 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'; + +export function initializeDatasources( + datasourceMap: Record, + datasourceStates: Record, + // TODO turn this into a single return value via promise + onInit: (id: string, state: unknown) => void, + onError: (e: { message: string }) => void, + references?: SavedObjectReference[] +) { + Object.entries(datasourceMap).forEach(([datasourceId, datasource]) => { + if (datasourceStates[datasourceId] && datasourceStates[datasourceId].isLoading) { + datasource + .initialize(datasourceStates[datasourceId].state || undefined, references) + .then((datasourceState) => { + onInit(datasourceId, datasourceState); + }) + .catch(onError); + } + }); +} + +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( + visualization: Visualization, + visualizationState: unknown, + datasources: Record, + persistedDatasourceStates: Record, + references?: SavedObjectReference[] +): Promise { + const datasourceStates: Record = {}; + + await Promise.all( + Object.entries(datasources).map(([datasourceId, datasource]) => { + if (persistedDatasourceStates[datasourceId]) { + return datasource + .initialize(persistedDatasourceStates[datasourceId], references) + .then((datasourceState) => { + datasourceStates[datasourceId] = { isLoading: false, state: datasourceState }; + }); + } + }) + ); + + 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 2f6dad10c28c8..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 @@ -63,9 +63,6 @@ describe('editor_frame state management', () => { testDatasource2: { internalState2: '' }, }, visualization: {}, - datasourceMetaData: { - numberFilterableIndexPatterns: 0, - }, query: { query: '', language: 'lucene' }, filters: [], }, @@ -381,7 +378,6 @@ describe('editor_frame state management', () => { doc: { id: 'b', state: { - datasourceMetaData: { numberFilterableIndexPatterns: 0 }, datasourceStates: { a: { foo: 'c' } }, visualization: { bar: 'd' }, query: { query: '', language: 'lucene' }, 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 fb6a23e90cf1d..7c093e80a8645 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'; @@ -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); 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..8b8b990dab1f5 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, @@ -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 844ea6179c461..04122b3126844 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 @@ -21,9 +21,6 @@ const savedVis: Document = { state: { visualization: {}, datasourceStates: {}, - datasourceMetaData: { - numberFilterableIndexPatterns: 0, - }, query: { query: '', language: 'lucene' }, 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 c2d4f94ebd947..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,7 +29,7 @@ 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'; @@ -63,7 +64,7 @@ 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, }; @@ -160,13 +161,36 @@ 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; @@ -198,9 +222,9 @@ export class Embeddable extends AbstractEmbeddable>; - visualizations: Record>; + documentToExpression: (doc: Document) => Promise; } export class EmbeddableFactory implements EmbeddableFactoryDefinition { @@ -77,73 +75,31 @@ export class EmbeddableFactory implements EmbeddableFactoryDefinition { indexPatternService, timefilter, expressionRenderer, + documentToExpression, uiActions, - datasources, - visualizations, } = await this.getStartServices(); const store = new SavedObjectIndexStore(savedObjectsClient); const savedVis = await store.load(savedObjectId); - const promises = getFilterableIndexPatternIds(savedVis).map(async (id) => { - try { - return await indexPatternService.get(id); - } catch (error) { - // Unable to load index pattern, ignore error as the index patterns are only used to - // configure the filter and query bar - there is still a good chance to get the visualization - // to show. - return null; - } - }); + const promises = savedVis.references + .filter(({ type }) => type === 'index-pattern') + .map(async ({ id }) => { + try { + return await indexPatternService.get(id); + } catch (error) { + // Unable to load index pattern, ignore error as the index patterns are only used to + // configure the filter and query bar - there is still a good chance to get the visualization + // to show. + return null; + } + }); const indexPatterns = ( await Promise.all(promises) ).filter((indexPattern: IndexPattern | null): indexPattern is IndexPattern => Boolean(indexPattern) ); - const datasourceStates: Record = {}; - - await Promise.all( - Object.entries(datasources).map(([datasourceId, datasource]) => { - if (savedVis.state.datasourceStates[datasourceId]) { - return datasource - .initialize(savedVis.state.datasourceStates[datasourceId], savedVis.references) - .then((datasourceState) => { - datasourceStates[datasourceId] = { isLoading: false, state: datasourceState }; - }); - } - }) - ); - - const datasourceLayers: Record = {}; - Object.keys(datasources) - .filter((id) => datasourceStates[id]) - .forEach((id) => { - const datasourceState = datasourceStates[id].state; - const datasource = datasources[id]; - - const layers = datasource.getLayers(datasourceState); - layers.forEach((layer) => { - datasourceLayers[layer] = datasource.getPublicAPI({ - state: datasourceState, - layerId: layer, - }); - }); - }); - - const framePublicAPI = { - datasourceLayers, - query: savedVis.state.query, - filters: savedVis.state.filters, - }; - - const expression = buildExpression({ - visualization: visualizations[savedVis.visualizationType!], - visualizationState: savedVis.state.visualization, - datasourceMap: datasources, - datasourceStates, - framePublicAPI, - removeDateRange: true, - }); + const expression = await documentToExpression(savedVis); return new Embeddable( timefilter, 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 98d8dc892abab..86b137851d9bd 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx @@ -80,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 05bf23860f516..fbe5beccab112 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> = []; + private async documentToExpression(doc: Document) { + const [resolvedDatasources, resolvedVisualizations] = await Promise.all([ + collectAsyncDefinitions(this.datasources), + collectAsyncDefinitions(this.visualizations), + ]); + + return await persistedStateToExpression( + resolvedVisualizations[doc.visualizationType!], + doc.state.visualization, + resolvedDatasources, + doc.state.datasourceStates, + doc.references + ); + } + public setup( core: CoreSetup, plugins: EditorFrameSetupPlugins @@ -78,6 +95,7 @@ export class EditorFrameService { coreHttp: coreStart.http, timefilter: deps.data.query.timefilter.timefilter, expressionRenderer: deps.expressions.ReactExpressionRenderer, + documentToExpression: this.documentToExpression, indexPatternService: deps.data.indexPatterns, uiActions: deps.uiActions, datasources: resolvedDatasources, 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 6012bb2f83af7..a2d7b9abf3e17 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts @@ -514,34 +514,6 @@ 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: ['1', '2'], - }); - }); - }); - describe('#getPublicAPI', () => { let publicAPI: DatasourcePublicAPI; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index de27defee5b59..2b4e419727848 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -189,14 +189,6 @@ export function getIndexPatternDatasource({ toExpression, - getMetaData(state: IndexPatternPrivateState) { - return { - filterableIndexPatterns: _.uniq( - Object.values(state.layers).map((layer) => layer.indexPatternId) - ), - }; - }, - renderDataPanel( domElement: Element, props: DatasourceDataPanelProps diff --git a/x-pack/plugins/lens/public/persistence/filter_references.ts b/x-pack/plugins/lens/public/persistence/filter_references.ts index 5eb3e743ec42a..9d64958213a61 100644 --- a/x-pack/plugins/lens/public/persistence/filter_references.ts +++ b/x-pack/plugins/lens/public/persistence/filter_references.ts @@ -6,24 +6,8 @@ import { Filter } from 'src/plugins/data/public'; import { SavedObjectReference } from 'kibana/public'; -import { Document } from './saved_object_store'; import { PersistableFilter } from '../../common'; -export function getFilterableIndexPatternIds(doc: Document) { - return new Array(doc.state.datasourceMetaData.numberFilterableIndexPatterns) - .fill(undefined) - .map((_, index) => { - return doc.references.find(({ name }) => name === `filterable-index-pattern-${index}`)?.id; - }) - .filter(Boolean) as string[]; -} - -export function filterableIndexPatternIdsToReferences(filterableIndexPatternIds: string[]) { - return filterableIndexPatternIds.map((id, index) => { - return { type: 'index-pattern', id, name: `filterable-index-pattern-${index}` }; - }); -} - export function extractFilterReferences( filters: Filter[] ): { persistableFilters: PersistableFilter[]; references: SavedObjectReference[] } { 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 8bd9538798364..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 @@ -32,9 +32,6 @@ describe('LensStore', () => { visualizationType: 'bar', references: [], state: { - datasourceMetaData: { - numberFilterableIndexPatterns: 0, - }, datasourceStates: { indexpattern: { type: 'index_pattern', indexPattern: '.kibana_test' }, }, @@ -51,9 +48,6 @@ describe('LensStore', () => { visualizationType: 'bar', references: [], state: { - datasourceMetaData: { - numberFilterableIndexPatterns: 0, - }, datasourceStates: { indexpattern: { type: 'index_pattern', indexPattern: '.kibana_test' }, }, @@ -71,9 +65,6 @@ describe('LensStore', () => { description: 'My doc', visualizationType: 'bar', state: { - datasourceMetaData: { - numberFilterableIndexPatterns: 0, - }, datasourceStates: { indexpattern: { type: 'index_pattern', indexPattern: '.kibana_test' }, }, @@ -96,7 +87,6 @@ describe('LensStore', () => { visualizationType: 'line', references: [], state: { - datasourceMetaData: { numberFilterableIndexPatterns: 0 }, datasourceStates: { indexpattern: { type: 'index_pattern', indexPattern: 'lotr' } }, visualization: { gear: ['staff', 'pointy hat'] }, query: { query: '', language: 'lucene' }, @@ -110,7 +100,6 @@ describe('LensStore', () => { visualizationType: 'line', references: [], state: { - datasourceMetaData: { numberFilterableIndexPatterns: 0 }, datasourceStates: { indexpattern: { type: 'index_pattern', indexPattern: 'lotr' } }, visualization: { gear: ['staff', 'pointy hat'] }, query: { query: '', language: 'lucene' }, @@ -138,7 +127,6 @@ describe('LensStore', () => { title: 'Even the very wise cannot see all ends.', visualizationType: 'line', state: { - datasourceMetaData: { numberFilterableIndexPatterns: 0 }, 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 761efcec4b1fd..e4609213ec792 100644 --- a/x-pack/plugins/lens/public/persistence/saved_object_store.ts +++ b/x-pack/plugins/lens/public/persistence/saved_object_store.ts @@ -19,9 +19,6 @@ export interface Document { title: string; description?: string; state: { - datasourceMetaData: { - numberFilterableIndexPatterns: number; - }; datasourceStates: Record; visualization: unknown; query: Query; diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 4feb6fd01f825..20f2ce6c56774 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -44,7 +44,7 @@ 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; @@ -130,10 +130,6 @@ export interface DatasourceSuggestion { keptLayerIds: string[]; } -export interface DatasourceMetaData { - filterableIndexPatterns: string[]; -} - export type StateSetter = (newState: T | ((prevState: T) => T)) => void; /** @@ -165,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>; diff --git a/x-pack/plugins/lens/server/__snapshots__/migrations.test.ts.snap b/x-pack/plugins/lens/server/__snapshots__/migrations.test.ts.snap index 2c70f2d1ca9ef..4979438dbd3d0 100644 --- a/x-pack/plugins/lens/server/__snapshots__/migrations.test.ts.snap +++ b/x-pack/plugins/lens/server/__snapshots__/migrations.test.ts.snap @@ -4,9 +4,6 @@ exports[`Lens migrations 7.10.0 references should produce a valid document 1`] = Object { "attributes": Object { "state": Object { - "datasourceMetaData": Object { - "numberFilterableIndexPatterns": 2, - }, "datasourceStates": Object { "indexpattern": Object { "layers": Object { @@ -175,16 +172,6 @@ Object { "name": "indexpattern-datasource-layer-9a27f85d-35a9-4246-81b2-48e7ee9b0707", "type": "index-pattern", }, - Object { - "id": "90943e30-9a47-11e8-b64d-95841ca0b247", - "name": "filterable-index-pattern-0", - "type": "index-pattern", - }, - Object { - "id": "ff959d40-b880-11e8-a6d9-e546fe2bba5f", - "name": "filterable-index-pattern-1", - "type": "index-pattern", - }, Object { "id": "90943e30-9a47-11e8-b64d-95841ca0b247", "name": "filter-index-pattern-0", diff --git a/x-pack/plugins/lens/server/migrations.test.ts b/x-pack/plugins/lens/server/migrations.test.ts index ce1a4160a0cca..676494dcab619 100644 --- a/x-pack/plugins/lens/server/migrations.test.ts +++ b/x-pack/plugins/lens/server/migrations.test.ts @@ -457,20 +457,9 @@ describe('Lens migrations', () => { ).toBeUndefined(); }); - it('should list references for filterable index pattern ids', () => { + it('should remove datsource meta data', () => { const result = migrations['7.10.0'](example, context); - expect( - result.references?.find((ref) => ref.name === 'filterable-index-pattern-0')?.id - ).toEqual('90943e30-9a47-11e8-b64d-95841ca0b247'); - expect( - result.references?.find((ref) => ref.name === 'filterable-index-pattern-1')?.id - ).toEqual('ff959d40-b880-11e8-a6d9-e546fe2bba5f'); - }); - - it('should remove filterable index patterns', () => { - const result = migrations['7.10.0'](example, context); - expect(result.attributes.state.datasourceMetaData.filterableIndexPatterns).toBeUndefined(); - expect(result.attributes.state.datasourceMetaData.numberFilterableIndexPatterns).toEqual(2); + expect(result.attributes.state.datasourceMetaData).toBeUndefined(); }); it('should list references for filters', () => { diff --git a/x-pack/plugins/lens/server/migrations.ts b/x-pack/plugins/lens/server/migrations.ts index 9db622d49c062..14d8ca1adc032 100644 --- a/x-pack/plugins/lens/server/migrations.ts +++ b/x-pack/plugins/lens/server/migrations.ts @@ -49,9 +49,6 @@ interface LensDocShape { visualizationType: string | null; title: string; state: { - datasourceMetaData: { - numberFilterableIndexPatterns: number; - }; datasourceStates: { // This is hardcoded as our only datasource indexpattern: { @@ -269,17 +266,6 @@ const extractReferences: SavedObjectMigrationFn { - return { type: 'index-pattern', id, name: `filterable-index-pattern-${index}` }; - }) - ); - // 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) { @@ -309,9 +295,6 @@ const extractReferences: SavedObjectMigrationFn Date: Thu, 20 Aug 2020 13:40:43 +0200 Subject: [PATCH 14/16] fix tests and types --- .../lens/public/app_plugin/app.test.tsx | 17 ++++-- x-pack/plugins/lens/public/app_plugin/app.tsx | 2 +- .../editor_frame/editor_frame.test.tsx | 32 +---------- .../editor_frame/editor_frame.tsx | 23 ++++---- .../editor_frame_service/editor_frame/save.ts | 3 +- .../editor_frame/state_helpers.ts | 53 +++++++++---------- .../editor_frame/suggestion_panel.test.tsx | 1 - .../editor_frame/suggestion_panel.tsx | 19 ++----- .../workspace_panel/workspace_panel.test.tsx | 19 +------ .../workspace_panel/workspace_panel.tsx | 2 +- .../embeddable/embeddable.test.tsx | 2 +- .../public/editor_frame_service/service.tsx | 2 +- x-pack/plugins/lens/server/migrations.ts | 2 +- 13 files changed, 62 insertions(+), 115 deletions(-) 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 777e8b8ab78a3..b1d1fbd40f485 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.test.tsx @@ -286,6 +286,7 @@ describe('Lens App', () => { query: 'fake query', filters: [], }, + references: [], }); await act(async () => { instance.setProps({ docId: '1234' }); @@ -1057,6 +1058,7 @@ describe('Lens App', () => { query: 'kuery', filters: [], }, + references: [], } as jest.ResolvedValue); }); @@ -1088,7 +1090,11 @@ describe('Lens App', () => { act(() => onChange({ filterableIndexPatterns: [], - doc: ({ id: undefined } as unknown) as Document, + doc: ({ + id: undefined, + + references: [], + } as unknown) as Document, isSaveable: true, }) ); @@ -1135,7 +1141,11 @@ describe('Lens App', () => { act(() => onChange({ filterableIndexPatterns: [], - doc: ({ id: '1234' } as unknown) as Document, + doc: ({ + id: '1234', + + references: [], + } as unknown) as Document, isSaveable: true, }) ); @@ -1163,6 +1173,7 @@ describe('Lens App', () => { doc: ({ id: '1234', title: 'My cool doc', + references: [], state: { query: 'kuery', filters: [], @@ -1192,7 +1203,7 @@ describe('Lens App', () => { act(() => onChange({ filterableIndexPatterns: [], - doc: ({ id: '1234' } as unknown) as Document, + doc: ({ id: '1234', references: [] } as unknown) as Document, isSaveable: true, }) ); diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx index 10701685ffff5..8c80e249b0010 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.tsx @@ -271,7 +271,7 @@ export function App({ indexPatternsForTopNav: indexPatterns, })); }) - .catch(() => { + .catch((e) => { setState((s) => ({ ...s, isLoading: false })); redirectTo(); 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 b7ebdb43bb747..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 @@ -422,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 [ @@ -529,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 [ @@ -1447,7 +1417,7 @@ describe('editor_frame', () => { mockDatasource.getLayers.mockReturnValue(['first']); mockDatasource.getPersistableState = jest.fn((x) => ({ state: x, - savedObjectReferences: [{ type: 'index-pattern', id: '1', name: '' }], + savedObjectReferences: [{ type: 'index-pattern', id: '1', name: 'index-pattern-0' }], })); mockVisualization.initialize.mockReturnValue({ initialState: true }); 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 aedeb5de634d1..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,7 +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 } 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'; @@ -63,20 +63,19 @@ export function EditorFrame(props: EditorFrameProps) { // prevents executing dispatch on unmounted component let isUnmounted = false; if (!allLoaded) { - initializeDatasources( - props.datasourceMap, - state.datasourceStates, - (datasourceId, datasourceState) => { + initializeDatasources(props.datasourceMap, state.datasourceStates, props.doc?.references) + .then((result) => { if (!isUnmounted) { - dispatch({ - type: 'UPDATE_DATASOURCE_STATE', - updater: datasourceState, - datasourceId, + Object.entries(result).forEach(([datasourceId, { state: datasourceState }]) => { + dispatch({ + type: 'UPDATE_DATASOURCE_STATE', + updater: datasourceState, + datasourceId, + }); }); } - }, - onError - ); + }) + .catch(onError); } return () => { isUnmounted = true; 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 fe4a2b10d3fb5..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 @@ -54,8 +54,7 @@ export function getSavedObjectFormat({ visualizationState: state.visualization.state, datasourceMap: activeDatasources, datasourceStates: state.datasourceStates, - framePublicAPI, - removeDateRange: true, + datasourceLayers: framePublicAPI.datasourceLayers, }); return { 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 index 655b44bca6cfe..abc2c0fb6f82f 100644 --- 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 @@ -9,24 +9,24 @@ import { Ast } from '@kbn/interpreter/common'; import { Datasource, DatasourcePublicAPI, Visualization } from '../../types'; import { buildExpression } from './expression_helpers'; -export function initializeDatasources( +export async function initializeDatasources( datasourceMap: Record, datasourceStates: Record, - // TODO turn this into a single return value via promise - onInit: (id: string, state: unknown) => void, - onError: (e: { message: string }) => void, references?: SavedObjectReference[] -) { - Object.entries(datasourceMap).forEach(([datasourceId, datasource]) => { - if (datasourceStates[datasourceId] && datasourceStates[datasourceId].isLoading) { - datasource - .initialize(datasourceStates[datasourceId].state || undefined, references) - .then((datasourceState) => { - onInit(datasourceId, datasourceState); - }) - .catch(onError); - } - }); +): Promise> { + 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( @@ -58,18 +58,15 @@ export async function persistedStateToExpression( persistedDatasourceStates: Record, references?: SavedObjectReference[] ): Promise { - const datasourceStates: Record = {}; - - await Promise.all( - Object.entries(datasources).map(([datasourceId, datasource]) => { - if (persistedDatasourceStates[datasourceId]) { - return datasource - .initialize(persistedDatasourceStates[datasourceId], references) - .then((datasourceState) => { - datasourceStates[datasourceId] = { isLoading: false, state: datasourceState }; - }); - } - }) + const datasourceStates = await initializeDatasources( + datasources, + Object.fromEntries( + Object.entries(persistedDatasourceStates).map(([id, state]) => [ + id, + { isLoading: false, state }, + ]) + ), + references ); const datasourceLayers = createDatasourceLayers(datasources, datasourceStates); @@ -80,5 +77,5 @@ export async function persistedStateToExpression( datasourceMap: datasources, datasourceStates, datasourceLayers, - }); + })!; } 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 7c093e80a8645..2bcb4345527fa 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 @@ -29,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'; @@ -265,15 +265,6 @@ export function SuggestionPanel({ } } - const expressionContext = { - query: frame.query, - filters: frame.filters, - timeRange: { - from: frame.dateRange.fromDate, - to: frame.dateRange.toDate, - }, - }; - return (
@@ -318,9 +309,7 @@ export function SuggestionPanel({ {currentVisualizationId && ( { "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 8b8b990dab1f5..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 @@ -130,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 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 04122b3126844..db0e66c6b2f6d 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 @@ -117,7 +117,7 @@ describe('embeddable', () => { expect(expressionRenderer.mock.calls[0][0].searchContext).toEqual({ timeRange, - query, + query: [query, savedVis.state.query], filters, }); }); 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 fbe5beccab112..0180fd2054693 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/service.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/service.tsx @@ -95,7 +95,7 @@ export class EditorFrameService { coreHttp: coreStart.http, timefilter: deps.data.query.timefilter.timefilter, expressionRenderer: deps.expressions.ReactExpressionRenderer, - documentToExpression: this.documentToExpression, + documentToExpression: this.documentToExpression.bind(this), indexPatternService: deps.data.indexPatterns, uiActions: deps.uiActions, datasources: resolvedDatasources, diff --git a/x-pack/plugins/lens/server/migrations.ts b/x-pack/plugins/lens/server/migrations.ts index 14d8ca1adc032..fdbfa1e455f60 100644 --- a/x-pack/plugins/lens/server/migrations.ts +++ b/x-pack/plugins/lens/server/migrations.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { cloneDeep, uniqBy } from 'lodash'; +import { cloneDeep } from 'lodash'; import { fromExpression, toExpression, Ast, ExpressionFunctionAST } from '@kbn/interpreter/common'; import { SavedObjectMigrationMap, From eacb355f6539a71f3fb59977c65c7f3716de492a Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Fri, 21 Aug 2020 12:01:26 +0200 Subject: [PATCH 15/16] remarks --- .../editor_frame/state_helpers.ts | 20 ++-- .../editor_frame/suggestion_panel.tsx | 6 +- .../embeddable/embeddable.test.tsx | 41 ++++++++ .../embeddable/embeddable_factory.ts | 2 +- .../public/editor_frame_service/service.tsx | 20 ++-- .../indexpattern_datasource/loader.test.ts | 75 ++++++++++++++ .../persistence/filter_references.test.ts | 99 +++++++++++++++++++ .../public/persistence/filter_references.ts | 2 +- 8 files changed, 240 insertions(+), 25 deletions(-) create mode 100644 x-pack/plugins/lens/public/persistence/filter_references.test.ts 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 index abc2c0fb6f82f..6deb9ffd37a06 100644 --- 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 @@ -8,12 +8,13 @@ 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[] -): Promise> { +) { const states: Record = {}; await Promise.all( Object.entries(datasourceMap).map(([datasourceId, datasource]) => { @@ -52,12 +53,17 @@ export function createDatasourceLayers( } export async function persistedStateToExpression( - visualization: Visualization, - visualizationState: unknown, datasources: Record, - persistedDatasourceStates: Record, - references?: SavedObjectReference[] -): Promise { + 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( @@ -77,5 +83,5 @@ export async function persistedStateToExpression( datasourceMap: datasources, datasourceStates, datasourceLayers, - })!; + }); } 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 2bcb4345527fa..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 @@ -113,7 +113,7 @@ const SuggestionPreview = ({ }: { onSelect: () => void; preview: { - expression?: Ast; + expression?: Ast | null; icon: IconType; title: string; }; @@ -309,7 +309,7 @@ export function SuggestionPanel({ {currentVisualizationId && ( { }); }); + 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, diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts index b9add5313c4ba..b8f9f8de1d286 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts @@ -34,7 +34,7 @@ interface StartServices { expressionRenderer: ReactExpressionRendererType; indexPatternService: IndexPatternsContract; uiActions?: UiActionsStart; - documentToExpression: (doc: Document) => Promise; + documentToExpression: (doc: Document) => Promise; } export class EmbeddableFactory implements EmbeddableFactoryDefinition { 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 0180fd2054693..5fc347179a032 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/service.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/service.tsx @@ -61,19 +61,19 @@ 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( - resolvedVisualizations[doc.visualizationType!], - doc.state.visualization, - resolvedDatasources, - doc.state.datasourceStates, - doc.references - ); + return await persistedStateToExpression(resolvedDatasources, resolvedVisualizations, doc); } public setup( @@ -85,10 +85,6 @@ export class EditorFrameService { const getStartServices = async () => { const [coreStart, deps] = await core.getStartServices(); - const [resolvedDatasources, resolvedVisualizations] = await Promise.all([ - collectAsyncDefinitions(this.datasources), - collectAsyncDefinitions(this.visualizations), - ]); return { capabilities: coreStart.application.capabilities, savedObjectsClient: coreStart.savedObjects.client, @@ -98,8 +94,6 @@ export class EditorFrameService { documentToExpression: this.documentToExpression.bind(this), indexPatternService: deps.data.indexPatterns, uiActions: deps.uiActions, - datasources: resolvedDatasources, - visualizations: resolvedVisualizations, }; }; 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 fa525d25be774..be8aec6c3774f 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 { IndexPatternPersistedState, IndexPatternPrivateState, IndexPatternField } from './types'; import { documentField } from './document_field'; @@ -423,6 +425,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/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 index 9d64958213a61..47564e510ce9c 100644 --- a/x-pack/plugins/lens/public/persistence/filter_references.ts +++ b/x-pack/plugins/lens/public/persistence/filter_references.ts @@ -50,7 +50,7 @@ export function injectFilterReferences( } return { ...filterRow, - meta: metaRest, + meta: { ...metaRest, index: reference.id }, }; }); } From 6e545ce09106ab59cbe4dac22f83c8f508234d1c Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Fri, 21 Aug 2020 12:11:52 +0200 Subject: [PATCH 16/16] fix loader test --- .../lens/public/indexpattern_datasource/loader.test.ts | 2 +- .../plugins/lens/public/indexpattern_datasource/loader.ts | 8 +------- 2 files changed, 2 insertions(+), 8 deletions(-) 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 4c64645382343..d80bf779a5d17 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts @@ -427,7 +427,7 @@ describe('loader', () => { indexPatterns: { '2': sampleIndexPatterns['2'], }, - layers: { layerb: { ...savedState.layers.layerb, indexPatternId: 'b' } }, + layers: { layerb: { ...savedState.layers.layerb, indexPatternId: '2' } }, }); expect(storage.set).toHaveBeenCalledWith('lens-settings', { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts index ece935275c749..24906790a9fc9 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts @@ -6,13 +6,7 @@ import _ from 'lodash'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; -import { - SavedObjectsClientContract, - SavedObjectAttributes, - HttpSetup, - SavedObjectReference, -} from 'kibana/public'; -import { SimpleSavedObject } from 'kibana/public'; +import { SavedObjectsClientContract, HttpSetup, SavedObjectReference } from 'kibana/public'; import { StateSetter } from '../types'; import { IndexPattern,