mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Lens] Register saved object references (#74523)
This commit is contained in:
parent
471b114089
commit
86f73cb0c2
51 changed files with 1609 additions and 659 deletions
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
@ -284,11 +284,11 @@ describe('Lens App', () => {
|
|||
(defaultArgs.docStorage.load as jest.Mock).mockResolvedValue({
|
||||
id: '1234',
|
||||
title: 'Daaaaaaadaumching!',
|
||||
expression: 'valid expression',
|
||||
state: {
|
||||
query: 'fake query',
|
||||
datasourceMetaData: { filterableIndexPatterns: [{ id: '1', title: 'saved' }] },
|
||||
filters: [],
|
||||
},
|
||||
references: [],
|
||||
});
|
||||
await act(async () => {
|
||||
instance.setProps({ docId: '1234' });
|
||||
|
@ -346,12 +346,11 @@ describe('Lens App', () => {
|
|||
args.editorFrame = frame;
|
||||
(args.docStorage.load as jest.Mock).mockResolvedValue({
|
||||
id: '1234',
|
||||
expression: 'valid expression',
|
||||
state: {
|
||||
query: 'fake query',
|
||||
filters: [{ query: { match_phrase: { src: 'test' } } }],
|
||||
datasourceMetaData: { filterableIndexPatterns: [{ id: '1', title: 'saved' }] },
|
||||
},
|
||||
references: [{ type: 'index-pattern', id: '1', name: 'index-pattern-0' }],
|
||||
});
|
||||
|
||||
instance = mount(<App {...args} />);
|
||||
|
@ -375,15 +374,13 @@ describe('Lens App', () => {
|
|||
expect(frame.mount).toHaveBeenCalledWith(
|
||||
expect.any(Element),
|
||||
expect.objectContaining({
|
||||
doc: {
|
||||
doc: expect.objectContaining({
|
||||
id: '1234',
|
||||
expression: 'valid expression',
|
||||
state: {
|
||||
state: expect.objectContaining({
|
||||
query: 'fake query',
|
||||
filters: [{ query: { match_phrase: { src: 'test' } } }],
|
||||
datasourceMetaData: { filterableIndexPatterns: [{ id: '1', title: 'saved' }] },
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
@ -444,7 +441,6 @@ describe('Lens App', () => {
|
|||
expression: 'valid expression',
|
||||
state: {
|
||||
query: 'kuery',
|
||||
datasourceMetaData: { filterableIndexPatterns: [{ id: '1', title: 'saved' }] },
|
||||
},
|
||||
} as jest.ResolvedValue<Document>);
|
||||
});
|
||||
|
@ -467,7 +463,12 @@ describe('Lens App', () => {
|
|||
}
|
||||
|
||||
async function save({
|
||||
lastKnownDoc = { expression: 'kibana 3' },
|
||||
lastKnownDoc = {
|
||||
references: [],
|
||||
state: {
|
||||
filters: [],
|
||||
},
|
||||
},
|
||||
initialDocId,
|
||||
...saveProps
|
||||
}: SaveProps & {
|
||||
|
@ -481,16 +482,14 @@ describe('Lens App', () => {
|
|||
args.editorFrame = frame;
|
||||
(args.docStorage.load as jest.Mock).mockResolvedValue({
|
||||
id: '1234',
|
||||
expression: 'kibana',
|
||||
references: [],
|
||||
state: {
|
||||
query: 'fake query',
|
||||
datasourceMetaData: { filterableIndexPatterns: [{ id: '1', title: 'saved' }] },
|
||||
filters: [],
|
||||
},
|
||||
});
|
||||
(args.docStorage.save as jest.Mock).mockImplementation(async ({ id }) => ({
|
||||
id: id || 'aaa',
|
||||
expression: 'kibana 2',
|
||||
}));
|
||||
|
||||
await act(async () => {
|
||||
|
@ -508,6 +507,7 @@ describe('Lens App', () => {
|
|||
onChange({
|
||||
filterableIndexPatterns: [],
|
||||
doc: { id: initialDocId, ...lastKnownDoc } as Document,
|
||||
isSaveable: true,
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -541,7 +541,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();
|
||||
|
@ -560,7 +561,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();
|
||||
|
@ -575,11 +577,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);
|
||||
|
||||
|
@ -595,11 +598,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);
|
||||
|
||||
|
@ -615,11 +619,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();
|
||||
|
||||
|
@ -639,7 +644,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,
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -663,11 +669,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);
|
||||
});
|
||||
|
@ -717,7 +724,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();
|
||||
|
@ -756,7 +764,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();
|
||||
|
@ -779,7 +788,6 @@ describe('Lens App', () => {
|
|||
expression: 'valid expression',
|
||||
state: {
|
||||
query: 'kuery',
|
||||
datasourceMetaData: { filterableIndexPatterns: [{ id: '1', title: 'saved' }] },
|
||||
},
|
||||
} as jest.ResolvedValue<Document>);
|
||||
});
|
||||
|
@ -824,8 +832,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,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -842,8 +851,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,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1078,11 +1088,11 @@ describe('Lens App', () => {
|
|||
(defaultArgs.docStorage.load as jest.Mock).mockResolvedValue({
|
||||
id: '1234',
|
||||
title: 'My cool doc',
|
||||
expression: 'valid expression',
|
||||
state: {
|
||||
query: 'kuery',
|
||||
datasourceMetaData: { filterableIndexPatterns: [{ id: '1', title: 'saved' }] },
|
||||
filters: [],
|
||||
},
|
||||
references: [],
|
||||
} as jest.ResolvedValue<Document>);
|
||||
});
|
||||
|
||||
|
@ -1114,7 +1124,12 @@ describe('Lens App', () => {
|
|||
act(() =>
|
||||
onChange({
|
||||
filterableIndexPatterns: [],
|
||||
doc: ({ id: undefined, expression: 'valid expression' } as unknown) as Document,
|
||||
doc: ({
|
||||
id: undefined,
|
||||
|
||||
references: [],
|
||||
} as unknown) as Document,
|
||||
isSaveable: true,
|
||||
})
|
||||
);
|
||||
instance.update();
|
||||
|
@ -1135,7 +1150,8 @@ describe('Lens App', () => {
|
|||
act(() =>
|
||||
onChange({
|
||||
filterableIndexPatterns: [],
|
||||
doc: ({ id: undefined, expression: 'valid expression' } as unknown) as Document,
|
||||
doc: ({ id: undefined, state: {} } as unknown) as Document,
|
||||
isSaveable: true,
|
||||
})
|
||||
);
|
||||
instance.update();
|
||||
|
@ -1159,7 +1175,12 @@ describe('Lens App', () => {
|
|||
act(() =>
|
||||
onChange({
|
||||
filterableIndexPatterns: [],
|
||||
doc: ({ id: '1234', expression: 'different expression' } as unknown) as Document,
|
||||
doc: ({
|
||||
id: '1234',
|
||||
|
||||
references: [],
|
||||
} as unknown) as Document,
|
||||
isSaveable: true,
|
||||
})
|
||||
);
|
||||
instance.update();
|
||||
|
@ -1183,7 +1204,16 @@ describe('Lens App', () => {
|
|||
act(() =>
|
||||
onChange({
|
||||
filterableIndexPatterns: [],
|
||||
doc: ({ id: '1234', expression: 'valid expression' } as unknown) as Document,
|
||||
doc: ({
|
||||
id: '1234',
|
||||
title: 'My cool doc',
|
||||
references: [],
|
||||
state: {
|
||||
query: 'kuery',
|
||||
filters: [],
|
||||
},
|
||||
} as unknown) as Document,
|
||||
isSaveable: true,
|
||||
})
|
||||
);
|
||||
instance.update();
|
||||
|
@ -1207,7 +1237,8 @@ describe('Lens App', () => {
|
|||
act(() =>
|
||||
onChange({
|
||||
filterableIndexPatterns: [],
|
||||
doc: ({ id: '1234', expression: null } as unknown) as Document,
|
||||
doc: ({ id: '1234', references: [] } as unknown) as Document,
|
||||
isSaveable: true,
|
||||
})
|
||||
);
|
||||
instance.update();
|
||||
|
|
|
@ -28,7 +28,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';
|
||||
|
@ -57,6 +57,7 @@ interface State {
|
|||
query: Query;
|
||||
filters: Filter[];
|
||||
savedQuery?: SavedQuery;
|
||||
isSaveable: boolean;
|
||||
}
|
||||
|
||||
export function App({
|
||||
|
@ -100,6 +101,7 @@ export function App({
|
|||
originatingApp,
|
||||
filters: data.query.filterManager.getFilters(),
|
||||
indicateNoData: false,
|
||||
isSaveable: false,
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -122,11 +124,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,15 +175,34 @@ export function App({
|
|||
history,
|
||||
]);
|
||||
|
||||
const getLastKnownDocWithoutPinnedFilters = useCallback(
|
||||
function () {
|
||||
if (!lastKnownDoc) return undefined;
|
||||
const [pinnedFilters, appFilters] = _.partition(
|
||||
injectFilterReferences(lastKnownDoc.state?.filters || [], lastKnownDoc.references),
|
||||
esFilters.isFilterPinned
|
||||
);
|
||||
return pinnedFilters?.length
|
||||
? {
|
||||
...lastKnownDoc,
|
||||
state: {
|
||||
...lastKnownDoc.state,
|
||||
filters: appFilters,
|
||||
},
|
||||
}
|
||||
: lastKnownDoc;
|
||||
},
|
||||
[lastKnownDoc]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
onAppLeave((actions) => {
|
||||
// Confirm when the user has made any changes to an existing doc
|
||||
// or when the user has configured something without saving
|
||||
if (
|
||||
core.application.capabilities.visualize.save &&
|
||||
(state.persistedDoc?.expression
|
||||
? !_.isEqual(lastKnownDoc?.expression, state.persistedDoc.expression)
|
||||
: lastKnownDoc?.expression)
|
||||
!_.isEqual(state.persistedDoc?.state, getLastKnownDocWithoutPinnedFilters()?.state) &&
|
||||
(state.isSaveable || state.persistedDoc)
|
||||
) {
|
||||
return actions.confirm(
|
||||
i18n.translate('xpack.lens.app.unsavedWorkMessage', {
|
||||
|
@ -199,7 +216,14 @@ export function App({
|
|||
return actions.default();
|
||||
}
|
||||
});
|
||||
}, [lastKnownDoc, onAppLeave, state.persistedDoc, core.application.capabilities.visualize.save]);
|
||||
}, [
|
||||
lastKnownDoc,
|
||||
onAppLeave,
|
||||
state.persistedDoc,
|
||||
state.isSaveable,
|
||||
core.application.capabilities.visualize.save,
|
||||
getLastKnownDocWithoutPinnedFilters,
|
||||
]);
|
||||
|
||||
// Sync Kibana breadcrumbs any time the saved document's title changes
|
||||
useEffect(() => {
|
||||
|
@ -248,13 +272,17 @@ export function App({
|
|||
.load(docId)
|
||||
.then((doc) => {
|
||||
getAllIndexPatterns(
|
||||
doc.state.datasourceMetaData.filterableIndexPatterns,
|
||||
_.uniq(
|
||||
doc.references.filter(({ type }) => type === 'index-pattern').map(({ id }) => id)
|
||||
),
|
||||
data.indexPatterns,
|
||||
core.notifications
|
||||
)
|
||||
.then((indexPatterns) => {
|
||||
// Don't overwrite any pinned filters
|
||||
data.query.filterManager.setAppFilters(doc.state.filters);
|
||||
data.query.filterManager.setAppFilters(
|
||||
injectFilterReferences(doc.state.filters, doc.references)
|
||||
);
|
||||
setState((s) => ({
|
||||
...s,
|
||||
isLoading: false,
|
||||
|
@ -264,13 +292,13 @@ export function App({
|
|||
indexPatternsForTopNav: indexPatterns,
|
||||
}));
|
||||
})
|
||||
.catch(() => {
|
||||
.catch((e) => {
|
||||
setState((s) => ({ ...s, isLoading: false }));
|
||||
|
||||
redirectTo();
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
.catch((e) => {
|
||||
setState((s) => ({ ...s, isLoading: false }));
|
||||
|
||||
core.notifications.toasts.addDanger(
|
||||
|
@ -306,22 +334,9 @@ export function App({
|
|||
if (!lastKnownDoc) {
|
||||
return;
|
||||
}
|
||||
const [pinnedFilters, appFilters] = _.partition(
|
||||
lastKnownDoc.state?.filters,
|
||||
esFilters.isFilterPinned
|
||||
);
|
||||
const lastDocWithoutPinned = pinnedFilters?.length
|
||||
? {
|
||||
...lastKnownDoc,
|
||||
state: {
|
||||
...lastKnownDoc.state,
|
||||
filters: appFilters,
|
||||
},
|
||||
}
|
||||
: lastKnownDoc;
|
||||
|
||||
const doc = {
|
||||
...lastDocWithoutPinned,
|
||||
...getLastKnownDocWithoutPinnedFilters()!,
|
||||
description: saveProps.newDescription,
|
||||
id: saveProps.newCopyOnSave ? undefined : lastKnownDoc.id,
|
||||
title: saveProps.newTitle,
|
||||
|
@ -411,7 +426,7 @@ export function App({
|
|||
emphasize: true,
|
||||
iconType: 'check',
|
||||
run: () => {
|
||||
if (isSaveable && lastKnownDoc) {
|
||||
if (savingPermitted) {
|
||||
runSave({
|
||||
newTitle: lastKnownDoc.title,
|
||||
newCopyOnSave: false,
|
||||
|
@ -421,7 +436,7 @@ export function App({
|
|||
}
|
||||
},
|
||||
testId: 'lnsApp_saveAndReturnButton',
|
||||
disableButton: !isSaveable,
|
||||
disableButton: !savingPermitted,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
|
@ -436,12 +451,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"
|
||||
|
@ -522,7 +537,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 }));
|
||||
}
|
||||
|
@ -530,8 +548,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)
|
||||
)
|
||||
) {
|
||||
|
@ -573,12 +591,12 @@ export function App({
|
|||
}
|
||||
|
||||
export async function getAllIndexPatterns(
|
||||
ids: Array<{ id: string }>,
|
||||
ids: string[],
|
||||
indexPatternsService: IndexPatternsContract,
|
||||
notifications: NotificationsStart
|
||||
): Promise<IndexPatternInstance[]> {
|
||||
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', {
|
||||
|
|
|
@ -50,20 +50,6 @@ describe('Datatable Visualization', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('#getPersistableState', () => {
|
||||
it('should persist the internal state', () => {
|
||||
const expectedState: DatatableVisualizationState = {
|
||||
layers: [
|
||||
{
|
||||
layerId: 'baz',
|
||||
columns: ['a', 'b', 'c'],
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(datatableVisualization.getPersistableState(expectedState)).toEqual(expectedState);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getLayerIds', () => {
|
||||
it('return the layer ids', () => {
|
||||
const state: DatatableVisualizationState = {
|
||||
|
@ -340,7 +326,10 @@ describe('Datatable Visualization', () => {
|
|||
label: 'label',
|
||||
});
|
||||
|
||||
const expression = datatableVisualization.toExpression({ layers: [layer] }, frame) as Ast;
|
||||
const expression = datatableVisualization.toExpression(
|
||||
{ layers: [layer] },
|
||||
frame.datasourceLayers
|
||||
) as Ast;
|
||||
const tableArgs = buildExpression(expression).findFunction('lens_datatable_columns');
|
||||
|
||||
expect(tableArgs).toHaveLength(1);
|
||||
|
|
|
@ -25,10 +25,7 @@ function newLayerState(layerId: string): LayerState {
|
|||
};
|
||||
}
|
||||
|
||||
export const datatableVisualization: Visualization<
|
||||
DatatableVisualizationState,
|
||||
DatatableVisualizationState
|
||||
> = {
|
||||
export const datatableVisualization: Visualization<DatatableVisualizationState> = {
|
||||
id: 'lnsDatatable',
|
||||
|
||||
visualizationTypes: [
|
||||
|
@ -75,8 +72,6 @@ export const datatableVisualization: Visualization<
|
|||
);
|
||||
},
|
||||
|
||||
getPersistableState: (state) => state,
|
||||
|
||||
getSuggestions({
|
||||
table,
|
||||
state,
|
||||
|
@ -186,9 +181,9 @@ export const datatableVisualization: Visualization<
|
|||
};
|
||||
},
|
||||
|
||||
toExpression(state, frame): Ast {
|
||||
toExpression(state, datasourceLayers): Ast {
|
||||
const layer = state.layers[0];
|
||||
const datasource = frame.datasourceLayers[layer.layerId];
|
||||
const datasource = datasourceLayers[layer.layerId];
|
||||
const originalOrder = datasource.getTableSpec().map(({ columnId }) => columnId);
|
||||
// When we add a column it could be empty, and therefore have no order
|
||||
const sortedColumns = Array.from(new Set(originalOrder.concat(layer.columns)));
|
||||
|
|
|
@ -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: {} }],
|
||||
};
|
||||
}
|
|
@ -124,7 +124,6 @@ export function LayerPanel(
|
|||
const nextPublicAPI = layerDatasource.getPublicAPI({
|
||||
state: newState,
|
||||
layerId,
|
||||
dateRange: props.framePublicAPI.dateRange,
|
||||
});
|
||||
const nextTable = new Set(
|
||||
nextPublicAPI.getTableSpec().map(({ columnId }) => columnId)
|
||||
|
|
|
@ -170,25 +170,22 @@ describe('editor_frame', () => {
|
|||
doc={{
|
||||
visualizationType: 'testVis',
|
||||
title: '',
|
||||
expression: '',
|
||||
state: {
|
||||
datasourceStates: {
|
||||
testDatasource: datasource1State,
|
||||
testDatasource2: datasource2State,
|
||||
},
|
||||
visualization: {},
|
||||
datasourceMetaData: {
|
||||
filterableIndexPatterns: [],
|
||||
},
|
||||
query: { query: '', language: 'lucene' },
|
||||
filters: [],
|
||||
},
|
||||
references: [],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
expect(mockDatasource.initialize).toHaveBeenCalledWith(datasource1State);
|
||||
expect(mockDatasource2.initialize).toHaveBeenCalledWith(datasource2State);
|
||||
expect(mockDatasource.initialize).toHaveBeenCalledWith(datasource1State, []);
|
||||
expect(mockDatasource2.initialize).toHaveBeenCalledWith(datasource2State, []);
|
||||
expect(mockDatasource3.initialize).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
@ -425,21 +422,6 @@ describe('editor_frame', () => {
|
|||
"function": "kibana",
|
||||
"type": "function",
|
||||
},
|
||||
Object {
|
||||
"arguments": Object {
|
||||
"filters": Array [
|
||||
"[]",
|
||||
],
|
||||
"query": Array [
|
||||
"{\\"query\\":\\"\\",\\"language\\":\\"lucene\\"}",
|
||||
],
|
||||
"timeRange": Array [
|
||||
"{\\"from\\":\\"\\",\\"to\\":\\"\\"}",
|
||||
],
|
||||
},
|
||||
"function": "kibana_context",
|
||||
"type": "function",
|
||||
},
|
||||
Object {
|
||||
"arguments": Object {
|
||||
"layerIds": Array [
|
||||
|
@ -499,19 +481,16 @@ describe('editor_frame', () => {
|
|||
doc={{
|
||||
visualizationType: 'testVis',
|
||||
title: '',
|
||||
expression: '',
|
||||
state: {
|
||||
datasourceStates: {
|
||||
testDatasource: {},
|
||||
testDatasource2: {},
|
||||
},
|
||||
visualization: {},
|
||||
datasourceMetaData: {
|
||||
filterableIndexPatterns: [],
|
||||
},
|
||||
query: { query: '', language: 'lucene' },
|
||||
filters: [],
|
||||
},
|
||||
references: [],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
@ -535,21 +514,6 @@ describe('editor_frame', () => {
|
|||
"function": "kibana",
|
||||
"type": "function",
|
||||
},
|
||||
Object {
|
||||
"arguments": Object {
|
||||
"filters": Array [
|
||||
"[]",
|
||||
],
|
||||
"query": Array [
|
||||
"{\\"query\\":\\"\\",\\"language\\":\\"lucene\\"}",
|
||||
],
|
||||
"timeRange": Array [
|
||||
"{\\"from\\":\\"\\",\\"to\\":\\"\\"}",
|
||||
],
|
||||
},
|
||||
"function": "kibana_context",
|
||||
"type": "function",
|
||||
},
|
||||
Object {
|
||||
"arguments": Object {
|
||||
"layerIds": Array [
|
||||
|
@ -747,19 +711,16 @@ describe('editor_frame', () => {
|
|||
doc={{
|
||||
visualizationType: 'testVis',
|
||||
title: '',
|
||||
expression: '',
|
||||
state: {
|
||||
datasourceStates: {
|
||||
testDatasource: {},
|
||||
testDatasource2: {},
|
||||
},
|
||||
visualization: {},
|
||||
datasourceMetaData: {
|
||||
filterableIndexPatterns: [],
|
||||
},
|
||||
query: { query: '', language: 'lucene' },
|
||||
filters: [],
|
||||
},
|
||||
references: [],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
@ -802,19 +763,16 @@ describe('editor_frame', () => {
|
|||
doc={{
|
||||
visualizationType: 'testVis',
|
||||
title: '',
|
||||
expression: '',
|
||||
state: {
|
||||
datasourceStates: {
|
||||
testDatasource: datasource1State,
|
||||
testDatasource2: datasource2State,
|
||||
},
|
||||
visualization: {},
|
||||
datasourceMetaData: {
|
||||
filterableIndexPatterns: [],
|
||||
},
|
||||
query: { query: '', language: 'lucene' },
|
||||
filters: [],
|
||||
},
|
||||
references: [],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
@ -842,7 +800,6 @@ describe('editor_frame', () => {
|
|||
|
||||
it('should give access to the datasource state in the datasource factory function', async () => {
|
||||
const datasourceState = {};
|
||||
const dateRange = { fromDate: 'now-1w', toDate: 'now' };
|
||||
mockDatasource.initialize.mockResolvedValue(datasourceState);
|
||||
mockDatasource.getLayers.mockReturnValue(['first']);
|
||||
|
||||
|
@ -850,7 +807,6 @@ describe('editor_frame', () => {
|
|||
mount(
|
||||
<EditorFrame
|
||||
{...getDefaultProps()}
|
||||
dateRange={dateRange}
|
||||
visualizationMap={{
|
||||
testVis: mockVisualization,
|
||||
}}
|
||||
|
@ -865,7 +821,6 @@ describe('editor_frame', () => {
|
|||
});
|
||||
|
||||
expect(mockDatasource.getPublicAPI).toHaveBeenCalledWith({
|
||||
dateRange,
|
||||
state: datasourceState,
|
||||
layerId: 'first',
|
||||
});
|
||||
|
@ -1460,9 +1415,10 @@ describe('editor_frame', () => {
|
|||
})
|
||||
);
|
||||
mockDatasource.getLayers.mockReturnValue(['first']);
|
||||
mockDatasource.getMetaData.mockReturnValue({
|
||||
filterableIndexPatterns: [{ id: '1', title: 'resolved' }],
|
||||
});
|
||||
mockDatasource.getPersistableState = jest.fn((x) => ({
|
||||
state: x,
|
||||
savedObjectReferences: [{ type: 'index-pattern', id: '1', name: 'index-pattern-0' }],
|
||||
}));
|
||||
mockVisualization.initialize.mockReturnValue({ initialState: true });
|
||||
|
||||
await act(async () => {
|
||||
|
@ -1487,14 +1443,20 @@ describe('editor_frame', () => {
|
|||
|
||||
expect(onChange).toHaveBeenCalledTimes(2);
|
||||
expect(onChange).toHaveBeenNthCalledWith(1, {
|
||||
filterableIndexPatterns: [{ id: '1', title: 'resolved' }],
|
||||
filterableIndexPatterns: ['1'],
|
||||
doc: {
|
||||
expression: '',
|
||||
id: undefined,
|
||||
description: undefined,
|
||||
references: [
|
||||
{
|
||||
id: '1',
|
||||
name: 'index-pattern-0',
|
||||
type: 'index-pattern',
|
||||
},
|
||||
],
|
||||
state: {
|
||||
visualization: null, // Not yet loaded
|
||||
datasourceMetaData: { filterableIndexPatterns: [{ id: '1', title: 'resolved' }] },
|
||||
datasourceStates: { testDatasource: undefined },
|
||||
datasourceStates: { testDatasource: {} },
|
||||
query: { query: '', language: 'lucene' },
|
||||
filters: [],
|
||||
},
|
||||
|
@ -1502,18 +1464,23 @@ describe('editor_frame', () => {
|
|||
type: 'lens',
|
||||
visualizationType: 'testVis',
|
||||
},
|
||||
isSaveable: false,
|
||||
});
|
||||
expect(onChange).toHaveBeenLastCalledWith({
|
||||
filterableIndexPatterns: [{ id: '1', title: 'resolved' }],
|
||||
filterableIndexPatterns: ['1'],
|
||||
doc: {
|
||||
expression: '',
|
||||
references: [
|
||||
{
|
||||
id: '1',
|
||||
name: 'index-pattern-0',
|
||||
type: 'index-pattern',
|
||||
},
|
||||
],
|
||||
description: undefined,
|
||||
id: undefined,
|
||||
state: {
|
||||
visualization: { initialState: true }, // Now loaded
|
||||
datasourceMetaData: {
|
||||
filterableIndexPatterns: [{ id: '1', title: 'resolved' }],
|
||||
},
|
||||
datasourceStates: { testDatasource: undefined },
|
||||
datasourceStates: { testDatasource: {} },
|
||||
query: { query: '', language: 'lucene' },
|
||||
filters: [],
|
||||
},
|
||||
|
@ -1521,6 +1488,7 @@ describe('editor_frame', () => {
|
|||
type: 'lens',
|
||||
visualizationType: 'testVis',
|
||||
},
|
||||
isSaveable: false,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1562,11 +1530,10 @@ describe('editor_frame', () => {
|
|||
expect(onChange).toHaveBeenNthCalledWith(3, {
|
||||
filterableIndexPatterns: [],
|
||||
doc: {
|
||||
expression: expect.stringContaining('vis "expression"'),
|
||||
id: undefined,
|
||||
references: [],
|
||||
state: {
|
||||
datasourceMetaData: { filterableIndexPatterns: [] },
|
||||
datasourceStates: { testDatasource: undefined },
|
||||
datasourceStates: { testDatasource: { datasource: '' } },
|
||||
visualization: { initialState: true },
|
||||
query: { query: 'new query', language: 'lucene' },
|
||||
filters: [],
|
||||
|
@ -1575,6 +1542,7 @@ describe('editor_frame', () => {
|
|||
type: 'lens',
|
||||
visualizationType: 'testVis',
|
||||
},
|
||||
isSaveable: true,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1583,9 +1551,10 @@ describe('editor_frame', () => {
|
|||
|
||||
mockDatasource.initialize.mockResolvedValue({});
|
||||
mockDatasource.getLayers.mockReturnValue(['first']);
|
||||
mockDatasource.getMetaData.mockReturnValue({
|
||||
filterableIndexPatterns: [{ id: '1', title: 'resolved' }],
|
||||
});
|
||||
mockDatasource.getPersistableState = jest.fn((x) => ({
|
||||
state: x,
|
||||
savedObjectReferences: [{ type: 'index-pattern', id: '1', name: '' }],
|
||||
}));
|
||||
mockVisualization.initialize.mockReturnValue({ initialState: true });
|
||||
|
||||
await act(async () => {
|
||||
|
|
|
@ -7,13 +7,7 @@
|
|||
import React, { useEffect, useReducer } from 'react';
|
||||
import { CoreSetup, CoreStart } from 'kibana/public';
|
||||
import { ReactExpressionRendererType } from '../../../../../../src/plugins/expressions/public';
|
||||
import {
|
||||
Datasource,
|
||||
DatasourcePublicAPI,
|
||||
FramePublicAPI,
|
||||
Visualization,
|
||||
DatasourceMetaData,
|
||||
} from '../../types';
|
||||
import { Datasource, FramePublicAPI, Visualization } from '../../types';
|
||||
import { reducer, getInitialState } from './state_management';
|
||||
import { DataPanelWrapper } from './data_panel_wrapper';
|
||||
import { ConfigPanelWrapper } from './config_panel';
|
||||
|
@ -26,6 +20,7 @@ import { getSavedObjectFormat } from './save';
|
|||
import { generateId } from '../../id_generator';
|
||||
import { Filter, Query, SavedQuery } from '../../../../../../src/plugins/data/public';
|
||||
import { EditorFrameStartPlugins } from '../service';
|
||||
import { initializeDatasources, createDatasourceLayers } from './state_helpers';
|
||||
|
||||
export interface EditorFrameProps {
|
||||
doc?: Document;
|
||||
|
@ -45,8 +40,9 @@ export interface EditorFrameProps {
|
|||
filters: Filter[];
|
||||
savedQuery?: SavedQuery;
|
||||
onChange: (arg: {
|
||||
filterableIndexPatterns: DatasourceMetaData['filterableIndexPatterns'];
|
||||
filterableIndexPatterns: string[];
|
||||
doc: Document;
|
||||
isSaveable: boolean;
|
||||
}) => void;
|
||||
showNoDataPopover: () => void;
|
||||
}
|
||||
|
@ -67,25 +63,19 @@ export function EditorFrame(props: EditorFrameProps) {
|
|||
// prevents executing dispatch on unmounted component
|
||||
let isUnmounted = false;
|
||||
if (!allLoaded) {
|
||||
Object.entries(props.datasourceMap).forEach(([datasourceId, datasource]) => {
|
||||
if (
|
||||
state.datasourceStates[datasourceId] &&
|
||||
state.datasourceStates[datasourceId].isLoading
|
||||
) {
|
||||
datasource
|
||||
.initialize(state.datasourceStates[datasourceId].state || undefined)
|
||||
.then((datasourceState) => {
|
||||
if (!isUnmounted) {
|
||||
dispatch({
|
||||
type: 'UPDATE_DATASOURCE_STATE',
|
||||
updater: datasourceState,
|
||||
datasourceId,
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(onError);
|
||||
}
|
||||
});
|
||||
initializeDatasources(props.datasourceMap, state.datasourceStates, props.doc?.references)
|
||||
.then((result) => {
|
||||
if (!isUnmounted) {
|
||||
Object.entries(result).forEach(([datasourceId, { state: datasourceState }]) => {
|
||||
dispatch({
|
||||
type: 'UPDATE_DATASOURCE_STATE',
|
||||
updater: datasourceState,
|
||||
datasourceId,
|
||||
});
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(onError);
|
||||
}
|
||||
return () => {
|
||||
isUnmounted = true;
|
||||
|
@ -95,22 +85,7 @@ export function EditorFrame(props: EditorFrameProps) {
|
|||
[allLoaded, onError]
|
||||
);
|
||||
|
||||
const datasourceLayers: Record<string, DatasourcePublicAPI> = {};
|
||||
Object.keys(props.datasourceMap)
|
||||
.filter((id) => state.datasourceStates[id] && !state.datasourceStates[id].isLoading)
|
||||
.forEach((id) => {
|
||||
const datasourceState = state.datasourceStates[id].state;
|
||||
const datasource = props.datasourceMap[id];
|
||||
|
||||
const layers = datasource.getLayers(datasourceState);
|
||||
layers.forEach((layer) => {
|
||||
datasourceLayers[layer] = props.datasourceMap[id].getPublicAPI({
|
||||
state: datasourceState,
|
||||
layerId: layer,
|
||||
dateRange: props.dateRange,
|
||||
});
|
||||
});
|
||||
});
|
||||
const datasourceLayers = createDatasourceLayers(props.datasourceMap, state.datasourceStates);
|
||||
|
||||
const framePublicAPI: FramePublicAPI = {
|
||||
datasourceLayers,
|
||||
|
@ -165,7 +140,18 @@ export function EditorFrame(props: EditorFrameProps) {
|
|||
if (props.doc) {
|
||||
dispatch({
|
||||
type: 'VISUALIZATION_LOADED',
|
||||
doc: props.doc,
|
||||
doc: {
|
||||
...props.doc,
|
||||
state: {
|
||||
...props.doc.state,
|
||||
visualization: props.doc.visualizationType
|
||||
? props.visualizationMap[props.doc.visualizationType].initialize(
|
||||
framePublicAPI,
|
||||
props.doc.state.visualization
|
||||
)
|
||||
: props.doc.state.visualization,
|
||||
},
|
||||
},
|
||||
});
|
||||
} else {
|
||||
dispatch({
|
||||
|
@ -206,36 +192,20 @@ export function EditorFrame(props: EditorFrameProps) {
|
|||
return;
|
||||
}
|
||||
|
||||
const indexPatterns: DatasourceMetaData['filterableIndexPatterns'] = [];
|
||||
Object.entries(props.datasourceMap)
|
||||
.filter(([id, datasource]) => {
|
||||
const stateWrapper = state.datasourceStates[id];
|
||||
return (
|
||||
stateWrapper &&
|
||||
!stateWrapper.isLoading &&
|
||||
datasource.getLayers(stateWrapper.state).length > 0
|
||||
);
|
||||
props.onChange(
|
||||
getSavedObjectFormat({
|
||||
activeDatasources: Object.keys(state.datasourceStates).reduce(
|
||||
(datasourceMap, datasourceId) => ({
|
||||
...datasourceMap,
|
||||
[datasourceId]: props.datasourceMap[datasourceId],
|
||||
}),
|
||||
{}
|
||||
),
|
||||
visualization: activeVisualization,
|
||||
state,
|
||||
framePublicAPI,
|
||||
})
|
||||
.forEach(([id, datasource]) => {
|
||||
indexPatterns.push(
|
||||
...datasource.getMetaData(state.datasourceStates[id].state).filterableIndexPatterns
|
||||
);
|
||||
});
|
||||
|
||||
const doc = getSavedObjectFormat({
|
||||
activeDatasources: Object.keys(state.datasourceStates).reduce(
|
||||
(datasourceMap, datasourceId) => ({
|
||||
...datasourceMap,
|
||||
[datasourceId]: props.datasourceMap[datasourceId],
|
||||
}),
|
||||
{}
|
||||
),
|
||||
visualization: activeVisualization,
|
||||
state,
|
||||
framePublicAPI,
|
||||
});
|
||||
|
||||
props.onChange({ filterableIndexPatterns: indexPatterns, doc });
|
||||
);
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[
|
||||
|
|
|
@ -5,8 +5,7 @@
|
|||
*/
|
||||
|
||||
import { Ast, fromExpression, ExpressionFunctionAST } from '@kbn/interpreter/common';
|
||||
import { Visualization, Datasource, FramePublicAPI } from '../../types';
|
||||
import { Filter, TimeRange, Query } from '../../../../../../src/plugins/data/public';
|
||||
import { Visualization, Datasource, DatasourcePublicAPI } from '../../types';
|
||||
|
||||
export function prependDatasourceExpression(
|
||||
visualizationExpression: Ast | string | null,
|
||||
|
@ -58,40 +57,12 @@ export function prependDatasourceExpression(
|
|||
? fromExpression(visualizationExpression)
|
||||
: visualizationExpression;
|
||||
|
||||
return {
|
||||
type: 'expression',
|
||||
chain: [datafetchExpression, ...parsedVisualizationExpression.chain],
|
||||
};
|
||||
}
|
||||
|
||||
export function prependKibanaContext(
|
||||
expression: Ast | string,
|
||||
{
|
||||
timeRange,
|
||||
query,
|
||||
filters,
|
||||
}: {
|
||||
timeRange?: TimeRange;
|
||||
query?: Query;
|
||||
filters?: Filter[];
|
||||
}
|
||||
): Ast {
|
||||
const parsedExpression = typeof expression === 'string' ? fromExpression(expression) : expression;
|
||||
|
||||
return {
|
||||
type: 'expression',
|
||||
chain: [
|
||||
{ type: 'function', function: 'kibana', arguments: {} },
|
||||
{
|
||||
type: 'function',
|
||||
function: 'kibana_context',
|
||||
arguments: {
|
||||
timeRange: timeRange ? [JSON.stringify(timeRange)] : [],
|
||||
query: query ? [JSON.stringify(query)] : [],
|
||||
filters: [JSON.stringify(filters || [])],
|
||||
},
|
||||
},
|
||||
...parsedExpression.chain,
|
||||
datafetchExpression,
|
||||
...parsedVisualizationExpression.chain,
|
||||
],
|
||||
};
|
||||
}
|
||||
|
@ -101,8 +72,7 @@ export function buildExpression({
|
|||
visualizationState,
|
||||
datasourceMap,
|
||||
datasourceStates,
|
||||
framePublicAPI,
|
||||
removeDateRange,
|
||||
datasourceLayers,
|
||||
}: {
|
||||
visualization: Visualization | null;
|
||||
visualizationState: unknown;
|
||||
|
@ -114,24 +84,12 @@ export function buildExpression({
|
|||
state: unknown;
|
||||
}
|
||||
>;
|
||||
framePublicAPI: FramePublicAPI;
|
||||
removeDateRange?: boolean;
|
||||
datasourceLayers: Record<string, DatasourcePublicAPI>;
|
||||
}): Ast | null {
|
||||
if (visualization === null) {
|
||||
return null;
|
||||
}
|
||||
const visualizationExpression = visualization.toExpression(visualizationState, framePublicAPI);
|
||||
|
||||
const expressionContext = removeDateRange
|
||||
? { query: framePublicAPI.query, filters: framePublicAPI.filters }
|
||||
: {
|
||||
query: framePublicAPI.query,
|
||||
timeRange: {
|
||||
from: framePublicAPI.dateRange.fromDate,
|
||||
to: framePublicAPI.dateRange.toDate,
|
||||
},
|
||||
filters: framePublicAPI.filters,
|
||||
};
|
||||
const visualizationExpression = visualization.toExpression(visualizationState, datasourceLayers);
|
||||
|
||||
const completeExpression = prependDatasourceExpression(
|
||||
visualizationExpression,
|
||||
|
@ -139,9 +97,5 @@ export function buildExpression({
|
|||
datasourceStates
|
||||
);
|
||||
|
||||
if (completeExpression) {
|
||||
return prependKibanaContext(completeExpression, expressionContext);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
return completeExpression;
|
||||
}
|
||||
|
|
|
@ -8,14 +8,18 @@ import { getSavedObjectFormat, Props } from './save';
|
|||
import { createMockDatasource, createMockVisualization } from '../mocks';
|
||||
import { esFilters, IIndexPattern, IFieldType } from '../../../../../../src/plugins/data/public';
|
||||
|
||||
jest.mock('./expression_helpers');
|
||||
|
||||
describe('save editor frame state', () => {
|
||||
const mockVisualization = createMockVisualization();
|
||||
mockVisualization.getPersistableState.mockImplementation((x) => x);
|
||||
const mockDatasource = createMockDatasource('a');
|
||||
const mockIndexPattern = ({ id: 'indexpattern' } as unknown) as IIndexPattern;
|
||||
const mockField = ({ name: '@timestamp' } as unknown) as IFieldType;
|
||||
|
||||
mockDatasource.getPersistableState.mockImplementation((x) => x);
|
||||
mockDatasource.getPersistableState.mockImplementation((x) => ({
|
||||
state: x,
|
||||
savedObjectReferences: [],
|
||||
}));
|
||||
const saveArgs: Props = {
|
||||
activeDatasources: {
|
||||
indexpattern: mockDatasource,
|
||||
|
@ -47,15 +51,17 @@ describe('save editor frame state', () => {
|
|||
it('transforms from internal state to persisted doc format', async () => {
|
||||
const datasource = createMockDatasource('a');
|
||||
datasource.getPersistableState.mockImplementation((state) => ({
|
||||
stuff: `${state}_datasource_persisted`,
|
||||
state: {
|
||||
stuff: `${state}_datasource_persisted`,
|
||||
},
|
||||
savedObjectReferences: [],
|
||||
}));
|
||||
datasource.toExpression.mockReturnValue('my | expr');
|
||||
|
||||
const visualization = createMockVisualization();
|
||||
visualization.getPersistableState.mockImplementation((state) => ({
|
||||
things: `${state}_vis_persisted`,
|
||||
}));
|
||||
visualization.toExpression.mockReturnValue('vis | expr');
|
||||
|
||||
const doc = await getSavedObjectFormat({
|
||||
const { doc, filterableIndexPatterns, isSaveable } = await getSavedObjectFormat({
|
||||
...saveArgs,
|
||||
activeDatasources: {
|
||||
indexpattern: datasource,
|
||||
|
@ -74,27 +80,32 @@ describe('save editor frame state', () => {
|
|||
visualization,
|
||||
});
|
||||
|
||||
expect(filterableIndexPatterns).toEqual([]);
|
||||
expect(isSaveable).toEqual(true);
|
||||
expect(doc).toEqual({
|
||||
id: undefined,
|
||||
expression: '',
|
||||
state: {
|
||||
datasourceMetaData: {
|
||||
filterableIndexPatterns: [],
|
||||
},
|
||||
datasourceStates: {
|
||||
indexpattern: {
|
||||
stuff: '2_datasource_persisted',
|
||||
},
|
||||
},
|
||||
visualization: { things: '4_vis_persisted' },
|
||||
visualization: '4',
|
||||
query: { query: '', language: 'lucene' },
|
||||
filters: [
|
||||
{
|
||||
meta: { index: 'indexpattern' },
|
||||
meta: { indexRefName: 'filter-index-pattern-0' },
|
||||
exists: { field: '@timestamp' },
|
||||
},
|
||||
],
|
||||
},
|
||||
references: [
|
||||
{
|
||||
id: 'indexpattern',
|
||||
name: 'filter-index-pattern-0',
|
||||
type: 'index-pattern',
|
||||
},
|
||||
],
|
||||
title: 'bbb',
|
||||
type: 'lens',
|
||||
visualizationType: '3',
|
||||
|
|
|
@ -5,11 +5,12 @@
|
|||
*/
|
||||
|
||||
import _ from 'lodash';
|
||||
import { toExpression } from '@kbn/interpreter/target/common';
|
||||
import { SavedObjectReference } from 'kibana/public';
|
||||
import { EditorFrameState } from './state_management';
|
||||
import { Document } from '../../persistence/saved_object_store';
|
||||
import { buildExpression } from './expression_helpers';
|
||||
import { Datasource, Visualization, FramePublicAPI } from '../../types';
|
||||
import { extractFilterReferences } from '../../persistence';
|
||||
import { buildExpression } from './expression_helpers';
|
||||
|
||||
export interface Props {
|
||||
activeDatasources: Record<string, Datasource>;
|
||||
|
@ -23,43 +24,55 @@ export function getSavedObjectFormat({
|
|||
state,
|
||||
visualization,
|
||||
framePublicAPI,
|
||||
}: Props): Document {
|
||||
}: Props): {
|
||||
doc: Document;
|
||||
filterableIndexPatterns: string[];
|
||||
isSaveable: boolean;
|
||||
} {
|
||||
const datasourceStates: Record<string, unknown> = {};
|
||||
const references: SavedObjectReference[] = [];
|
||||
Object.entries(activeDatasources).forEach(([id, datasource]) => {
|
||||
const { state: persistableState, savedObjectReferences } = datasource.getPersistableState(
|
||||
state.datasourceStates[id].state
|
||||
);
|
||||
datasourceStates[id] = persistableState;
|
||||
references.push(...savedObjectReferences);
|
||||
});
|
||||
|
||||
const uniqueFilterableIndexPatternIds = _.uniq(
|
||||
references.filter(({ type }) => type === 'index-pattern').map(({ id }) => id)
|
||||
);
|
||||
|
||||
const { persistableFilters, references: filterReferences } = extractFilterReferences(
|
||||
framePublicAPI.filters
|
||||
);
|
||||
|
||||
references.push(...filterReferences);
|
||||
|
||||
const expression = buildExpression({
|
||||
visualization,
|
||||
visualizationState: state.visualization.state,
|
||||
datasourceMap: activeDatasources,
|
||||
datasourceStates: state.datasourceStates,
|
||||
framePublicAPI,
|
||||
removeDateRange: true,
|
||||
});
|
||||
|
||||
const datasourceStates: Record<string, unknown> = {};
|
||||
Object.entries(activeDatasources).forEach(([id, datasource]) => {
|
||||
datasourceStates[id] = datasource.getPersistableState(state.datasourceStates[id].state);
|
||||
});
|
||||
|
||||
const filterableIndexPatterns: Array<{ id: string; title: string }> = [];
|
||||
Object.entries(activeDatasources).forEach(([id, datasource]) => {
|
||||
filterableIndexPatterns.push(
|
||||
...datasource.getMetaData(state.datasourceStates[id].state).filterableIndexPatterns
|
||||
);
|
||||
datasourceLayers: framePublicAPI.datasourceLayers,
|
||||
});
|
||||
|
||||
return {
|
||||
id: state.persistedId,
|
||||
title: state.title,
|
||||
description: state.description,
|
||||
type: 'lens',
|
||||
visualizationType: state.visualization.activeId,
|
||||
expression: expression ? toExpression(expression) : '',
|
||||
state: {
|
||||
datasourceStates,
|
||||
datasourceMetaData: {
|
||||
filterableIndexPatterns: _.uniqBy(filterableIndexPatterns, 'id'),
|
||||
doc: {
|
||||
id: state.persistedId,
|
||||
title: state.title,
|
||||
description: state.description,
|
||||
type: 'lens',
|
||||
visualizationType: state.visualization.activeId,
|
||||
state: {
|
||||
datasourceStates,
|
||||
visualization: state.visualization.state,
|
||||
query: framePublicAPI.query,
|
||||
filters: persistableFilters,
|
||||
},
|
||||
visualization: visualization.getPersistableState(state.visualization.state),
|
||||
query: framePublicAPI.query,
|
||||
filters: framePublicAPI.filters,
|
||||
references,
|
||||
},
|
||||
filterableIndexPatterns: uniqueFilterableIndexPatternIds,
|
||||
isSaveable: expression !== null,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { SavedObjectReference } from 'kibana/public';
|
||||
import { Ast } from '@kbn/interpreter/common';
|
||||
import { Datasource, DatasourcePublicAPI, Visualization } from '../../types';
|
||||
import { buildExpression } from './expression_helpers';
|
||||
import { Document } from '../../persistence/saved_object_store';
|
||||
|
||||
export async function initializeDatasources(
|
||||
datasourceMap: Record<string, Datasource>,
|
||||
datasourceStates: Record<string, { state: unknown; isLoading: boolean }>,
|
||||
references?: SavedObjectReference[]
|
||||
) {
|
||||
const states: Record<string, { isLoading: boolean; state: unknown }> = {};
|
||||
await Promise.all(
|
||||
Object.entries(datasourceMap).map(([datasourceId, datasource]) => {
|
||||
if (datasourceStates[datasourceId]) {
|
||||
return datasource
|
||||
.initialize(datasourceStates[datasourceId].state || undefined, references)
|
||||
.then((datasourceState) => {
|
||||
states[datasourceId] = { isLoading: false, state: datasourceState };
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
return states;
|
||||
}
|
||||
|
||||
export function createDatasourceLayers(
|
||||
datasourceMap: Record<string, Datasource>,
|
||||
datasourceStates: Record<string, { state: unknown; isLoading: boolean }>
|
||||
) {
|
||||
const datasourceLayers: Record<string, DatasourcePublicAPI> = {};
|
||||
Object.keys(datasourceMap)
|
||||
.filter((id) => datasourceStates[id] && !datasourceStates[id].isLoading)
|
||||
.forEach((id) => {
|
||||
const datasourceState = datasourceStates[id].state;
|
||||
const datasource = datasourceMap[id];
|
||||
|
||||
const layers = datasource.getLayers(datasourceState);
|
||||
layers.forEach((layer) => {
|
||||
datasourceLayers[layer] = datasourceMap[id].getPublicAPI({
|
||||
state: datasourceState,
|
||||
layerId: layer,
|
||||
});
|
||||
});
|
||||
});
|
||||
return datasourceLayers;
|
||||
}
|
||||
|
||||
export async function persistedStateToExpression(
|
||||
datasources: Record<string, Datasource>,
|
||||
visualizations: Record<string, Visualization>,
|
||||
doc: Document
|
||||
): Promise<Ast | null> {
|
||||
const {
|
||||
state: { visualization: visualizationState, datasourceStates: persistedDatasourceStates },
|
||||
visualizationType,
|
||||
references,
|
||||
} = doc;
|
||||
if (!visualizationType) return null;
|
||||
const visualization = visualizations[visualizationType!];
|
||||
const datasourceStates = await initializeDatasources(
|
||||
datasources,
|
||||
Object.fromEntries(
|
||||
Object.entries(persistedDatasourceStates).map(([id, state]) => [
|
||||
id,
|
||||
{ isLoading: false, state },
|
||||
])
|
||||
),
|
||||
references
|
||||
);
|
||||
|
||||
const datasourceLayers = createDatasourceLayers(datasources, datasourceStates);
|
||||
|
||||
return buildExpression({
|
||||
visualization,
|
||||
visualizationState,
|
||||
datasourceMap: datasources,
|
||||
datasourceStates,
|
||||
datasourceLayers,
|
||||
});
|
||||
}
|
|
@ -57,19 +57,16 @@ describe('editor_frame state management', () => {
|
|||
const initialState = getInitialState({
|
||||
...props,
|
||||
doc: {
|
||||
expression: '',
|
||||
state: {
|
||||
datasourceStates: {
|
||||
testDatasource: { internalState1: '' },
|
||||
testDatasource2: { internalState2: '' },
|
||||
},
|
||||
visualization: {},
|
||||
datasourceMetaData: {
|
||||
filterableIndexPatterns: [],
|
||||
},
|
||||
query: { query: '', language: 'lucene' },
|
||||
filters: [],
|
||||
},
|
||||
references: [],
|
||||
title: '',
|
||||
visualizationType: 'testVis',
|
||||
},
|
||||
|
@ -380,9 +377,7 @@ describe('editor_frame state management', () => {
|
|||
type: 'VISUALIZATION_LOADED',
|
||||
doc: {
|
||||
id: 'b',
|
||||
expression: '',
|
||||
state: {
|
||||
datasourceMetaData: { filterableIndexPatterns: [] },
|
||||
datasourceStates: { a: { foo: 'c' } },
|
||||
visualization: { bar: 'd' },
|
||||
query: { query: '', language: 'lucene' },
|
||||
|
@ -392,6 +387,7 @@ describe('editor_frame state management', () => {
|
|||
description: 'My lens',
|
||||
type: 'lens',
|
||||
visualizationType: 'line',
|
||||
references: [],
|
||||
},
|
||||
}
|
||||
);
|
||||
|
|
|
@ -107,7 +107,7 @@ export function getSuggestions({
|
|||
* title and preview expression.
|
||||
*/
|
||||
function getVisualizationSuggestions(
|
||||
visualization: Visualization<unknown, unknown>,
|
||||
visualization: Visualization<unknown>,
|
||||
table: TableSuggestion,
|
||||
visualizationId: string,
|
||||
datasourceSuggestion: DatasourceSuggestion & { datasourceId: string },
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -21,6 +21,7 @@ import { IconType } from '@elastic/eui/src/components/icon/icon';
|
|||
import { Ast, toExpression } from '@kbn/interpreter/common';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import classNames from 'classnames';
|
||||
import { ExecutionContextSearch } from 'src/plugins/expressions';
|
||||
import { Action, PreviewState } from './state_management';
|
||||
import { Datasource, Visualization, FramePublicAPI, DatasourcePublicAPI } from '../../types';
|
||||
import { getSuggestions, switchToSuggestion } from './suggestion_helpers';
|
||||
|
@ -28,7 +29,7 @@ import {
|
|||
ReactExpressionRendererProps,
|
||||
ReactExpressionRendererType,
|
||||
} from '../../../../../../src/plugins/expressions/public';
|
||||
import { prependDatasourceExpression, prependKibanaContext } from './expression_helpers';
|
||||
import { prependDatasourceExpression } from './expression_helpers';
|
||||
import { debouncedComponent } from '../../debounced_component';
|
||||
import { trackUiEvent, trackSuggestionEvent } from '../../lens_ui_telemetry';
|
||||
import { DataPublicPluginStart } from '../../../../../../src/plugins/data/public';
|
||||
|
@ -112,7 +113,7 @@ const SuggestionPreview = ({
|
|||
}: {
|
||||
onSelect: () => void;
|
||||
preview: {
|
||||
expression?: Ast;
|
||||
expression?: Ast | null;
|
||||
icon: IconType;
|
||||
title: string;
|
||||
};
|
||||
|
@ -215,12 +216,24 @@ export function SuggestionPanel({
|
|||
visualizationMap,
|
||||
]);
|
||||
|
||||
const context: ExecutionContextSearch = useMemo(
|
||||
() => ({
|
||||
query: frame.query,
|
||||
timeRange: {
|
||||
from: frame.dateRange.fromDate,
|
||||
to: frame.dateRange.toDate,
|
||||
},
|
||||
filters: frame.filters,
|
||||
}),
|
||||
[frame.query, frame.dateRange.fromDate, frame.dateRange.toDate, frame.filters]
|
||||
);
|
||||
|
||||
const AutoRefreshExpressionRenderer = useMemo(() => {
|
||||
const autoRefreshFetch$ = plugins.data.query.timefilter.timefilter.getAutoRefreshFetch$();
|
||||
return (props: ReactExpressionRendererProps) => (
|
||||
<ExpressionRendererComponent {...props} reload$={autoRefreshFetch$} />
|
||||
<ExpressionRendererComponent {...props} searchContext={context} reload$={autoRefreshFetch$} />
|
||||
);
|
||||
}, [plugins.data.query.timefilter.timefilter]);
|
||||
}, [plugins.data.query.timefilter.timefilter, context]);
|
||||
|
||||
const [lastSelectedSuggestion, setLastSelectedSuggestion] = useState<number>(-1);
|
||||
|
||||
|
@ -252,15 +265,6 @@ export function SuggestionPanel({
|
|||
}
|
||||
}
|
||||
|
||||
const expressionContext = {
|
||||
query: frame.query,
|
||||
filters: frame.filters,
|
||||
timeRange: {
|
||||
from: frame.dateRange.fromDate,
|
||||
to: frame.dateRange.toDate,
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="lnsSuggestionPanel">
|
||||
<EuiFlexGroup alignItems="center">
|
||||
|
@ -305,9 +309,7 @@ export function SuggestionPanel({
|
|||
{currentVisualizationId && (
|
||||
<SuggestionPreview
|
||||
preview={{
|
||||
expression: currentStateExpression
|
||||
? prependKibanaContext(currentStateExpression, expressionContext)
|
||||
: undefined,
|
||||
expression: currentStateExpression,
|
||||
icon:
|
||||
visualizationMap[currentVisualizationId].getDescription(currentVisualizationState)
|
||||
.icon || 'empty',
|
||||
|
@ -325,9 +327,7 @@ export function SuggestionPanel({
|
|||
return (
|
||||
<SuggestionPreview
|
||||
preview={{
|
||||
expression: suggestion.previewExpression
|
||||
? prependKibanaContext(suggestion.previewExpression, expressionContext)
|
||||
: undefined,
|
||||
expression: suggestion.previewExpression,
|
||||
icon: suggestion.previewIcon,
|
||||
title: suggestion.title,
|
||||
}}
|
||||
|
@ -391,7 +391,6 @@ function getPreviewExpression(
|
|||
if (updatedLayerApis[layerId]) {
|
||||
updatedLayerApis[layerId] = datasource.getPublicAPI({
|
||||
layerId,
|
||||
dateRange: frame.dateRange,
|
||||
state: datasourceState,
|
||||
});
|
||||
}
|
||||
|
@ -400,7 +399,7 @@ function getPreviewExpression(
|
|||
|
||||
return visualization.toPreviewExpression(
|
||||
visualizableState.visualizationState,
|
||||
suggestionFrameApi
|
||||
suggestionFrameApi.datasourceLayers
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -260,7 +260,7 @@ export function ChartSwitch(props: Props) {
|
|||
function getTopSuggestion(
|
||||
props: Props,
|
||||
visualizationId: string,
|
||||
newVisualization: Visualization<unknown, unknown>,
|
||||
newVisualization: Visualization<unknown>,
|
||||
subVisualizationId?: string
|
||||
): Suggestion | undefined {
|
||||
const unfilteredSuggestions = getSuggestions({
|
||||
|
|
|
@ -172,21 +172,6 @@ describe('workspace_panel', () => {
|
|||
"function": "kibana",
|
||||
"type": "function",
|
||||
},
|
||||
Object {
|
||||
"arguments": Object {
|
||||
"filters": Array [
|
||||
"[]",
|
||||
],
|
||||
"query": Array [
|
||||
"{\\"query\\":\\"\\",\\"language\\":\\"lucene\\"}",
|
||||
],
|
||||
"timeRange": Array [
|
||||
"{\\"from\\":\\"now-7d\\",\\"to\\":\\"now\\"}",
|
||||
],
|
||||
},
|
||||
"function": "kibana_context",
|
||||
"type": "function",
|
||||
},
|
||||
Object {
|
||||
"arguments": Object {
|
||||
"layerIds": Array [
|
||||
|
@ -305,10 +290,10 @@ describe('workspace_panel', () => {
|
|||
);
|
||||
|
||||
expect(
|
||||
(instance.find(expressionRendererMock).prop('expression') as Ast).chain[2].arguments.layerIds
|
||||
(instance.find(expressionRendererMock).prop('expression') as Ast).chain[1].arguments.layerIds
|
||||
).toEqual(['first', 'second', 'third']);
|
||||
expect(
|
||||
(instance.find(expressionRendererMock).prop('expression') as Ast).chain[2].arguments.tables
|
||||
(instance.find(expressionRendererMock).prop('expression') as Ast).chain[1].arguments.tables
|
||||
).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
|
|
|
@ -18,6 +18,7 @@ import {
|
|||
EuiLink,
|
||||
} from '@elastic/eui';
|
||||
import { CoreStart, CoreSetup } from 'kibana/public';
|
||||
import { ExecutionContextSearch } from 'src/plugins/expressions';
|
||||
import {
|
||||
ExpressionRendererEvent,
|
||||
ReactExpressionRendererType,
|
||||
|
@ -129,7 +130,7 @@ export function InnerWorkspacePanel({
|
|||
visualizationState,
|
||||
datasourceMap,
|
||||
datasourceStates,
|
||||
framePublicAPI,
|
||||
datasourceLayers: framePublicAPI.datasourceLayers,
|
||||
});
|
||||
} catch (e) {
|
||||
// Most likely an error in the expression provided by a datasource or visualization
|
||||
|
@ -173,6 +174,23 @@ export function InnerWorkspacePanel({
|
|||
[plugins.data.query.timefilter.timefilter]
|
||||
);
|
||||
|
||||
const context: ExecutionContextSearch = useMemo(
|
||||
() => ({
|
||||
query: framePublicAPI.query,
|
||||
timeRange: {
|
||||
from: framePublicAPI.dateRange.fromDate,
|
||||
to: framePublicAPI.dateRange.toDate,
|
||||
},
|
||||
filters: framePublicAPI.filters,
|
||||
}),
|
||||
[
|
||||
framePublicAPI.query,
|
||||
framePublicAPI.dateRange.fromDate,
|
||||
framePublicAPI.dateRange.toDate,
|
||||
framePublicAPI.filters,
|
||||
]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// reset expression error if component attempts to run it again
|
||||
if (expression && localState.expressionBuildError) {
|
||||
|
@ -264,6 +282,7 @@ export function InnerWorkspacePanel({
|
|||
className="lnsExpressionRenderer__component"
|
||||
padding="m"
|
||||
expression={expression!}
|
||||
searchContext={context}
|
||||
reload$={autoRefreshFetch$}
|
||||
onEvent={onEvent}
|
||||
renderError={(errorMessage?: string | null) => {
|
||||
|
|
|
@ -18,16 +18,13 @@ jest.mock('../../../../../../src/plugins/inspector/public/', () => ({
|
|||
}));
|
||||
|
||||
const savedVis: Document = {
|
||||
expression: 'my | expression',
|
||||
state: {
|
||||
visualization: {},
|
||||
datasourceStates: {},
|
||||
datasourceMetaData: {
|
||||
filterableIndexPatterns: [],
|
||||
},
|
||||
query: { query: '', language: 'lucene' },
|
||||
filters: [],
|
||||
},
|
||||
references: [],
|
||||
title: 'My title',
|
||||
visualizationType: '',
|
||||
};
|
||||
|
@ -59,13 +56,14 @@ describe('embeddable', () => {
|
|||
editUrl: '',
|
||||
editable: true,
|
||||
savedVis,
|
||||
expression: 'my | expression',
|
||||
},
|
||||
{ id: '123' }
|
||||
);
|
||||
embeddable.render(mountpoint);
|
||||
|
||||
expect(expressionRenderer).toHaveBeenCalledTimes(1);
|
||||
expect(expressionRenderer.mock.calls[0][0]!.expression).toEqual(savedVis.expression);
|
||||
expect(expressionRenderer.mock.calls[0][0]!.expression).toEqual('my | expression');
|
||||
});
|
||||
|
||||
it('should re-render if new input is pushed', () => {
|
||||
|
@ -82,6 +80,7 @@ describe('embeddable', () => {
|
|||
editUrl: '',
|
||||
editable: true,
|
||||
savedVis,
|
||||
expression: 'my | expression',
|
||||
},
|
||||
{ id: '123' }
|
||||
);
|
||||
|
@ -110,6 +109,7 @@ describe('embeddable', () => {
|
|||
editUrl: '',
|
||||
editable: true,
|
||||
savedVis,
|
||||
expression: 'my | expression',
|
||||
},
|
||||
{ id: '123', timeRange, query, filters }
|
||||
);
|
||||
|
@ -117,11 +117,52 @@ describe('embeddable', () => {
|
|||
|
||||
expect(expressionRenderer.mock.calls[0][0].searchContext).toEqual({
|
||||
timeRange,
|
||||
query,
|
||||
query: [query, savedVis.state.query],
|
||||
filters,
|
||||
});
|
||||
});
|
||||
|
||||
it('should merge external context with query and filters of the saved object', () => {
|
||||
const timeRange: TimeRange = { from: 'now-15d', to: 'now' };
|
||||
const query: Query = { language: 'kquery', query: 'external filter' };
|
||||
const filters: Filter[] = [{ meta: { alias: 'test', negate: false, disabled: false } }];
|
||||
|
||||
const embeddable = new Embeddable(
|
||||
dataPluginMock.createSetupContract().query.timefilter.timefilter,
|
||||
expressionRenderer,
|
||||
getTrigger,
|
||||
{
|
||||
editPath: '',
|
||||
editUrl: '',
|
||||
editable: true,
|
||||
savedVis: {
|
||||
...savedVis,
|
||||
state: {
|
||||
...savedVis.state,
|
||||
query: { language: 'kquery', query: 'saved filter' },
|
||||
filters: [
|
||||
{ meta: { alias: 'test', negate: false, disabled: false, indexRefName: 'filter-0' } },
|
||||
],
|
||||
},
|
||||
references: [{ type: 'index-pattern', name: 'filter-0', id: 'my-index-pattern-id' }],
|
||||
},
|
||||
expression: 'my | expression',
|
||||
},
|
||||
{ id: '123', timeRange, query, filters }
|
||||
);
|
||||
embeddable.render(mountpoint);
|
||||
|
||||
expect(expressionRenderer.mock.calls[0][0].searchContext).toEqual({
|
||||
timeRange,
|
||||
query: [query, { language: 'kquery', query: 'saved filter' }],
|
||||
filters: [
|
||||
filters[0],
|
||||
// actual index pattern id gets injected
|
||||
{ meta: { alias: 'test', negate: false, disabled: false, index: 'my-index-pattern-id' } },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should execute trigger on event from expression renderer', () => {
|
||||
const embeddable = new Embeddable(
|
||||
dataPluginMock.createSetupContract().query.timefilter.timefilter,
|
||||
|
@ -132,6 +173,7 @@ describe('embeddable', () => {
|
|||
editUrl: '',
|
||||
editable: true,
|
||||
savedVis,
|
||||
expression: 'my | expression',
|
||||
},
|
||||
{ id: '123' }
|
||||
);
|
||||
|
@ -162,6 +204,7 @@ describe('embeddable', () => {
|
|||
editUrl: '',
|
||||
editable: true,
|
||||
savedVis,
|
||||
expression: 'my | expression',
|
||||
},
|
||||
{ id: '123', timeRange, query, filters }
|
||||
);
|
||||
|
@ -195,6 +238,7 @@ describe('embeddable', () => {
|
|||
editUrl: '',
|
||||
editable: true,
|
||||
savedVis,
|
||||
expression: 'my | expression',
|
||||
},
|
||||
{ id: '123', timeRange, query, filters }
|
||||
);
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
TimefilterContract,
|
||||
TimeRange,
|
||||
} from 'src/plugins/data/public';
|
||||
import { ExecutionContextSearch } from 'src/plugins/expressions';
|
||||
|
||||
import { Subscription } from 'rxjs';
|
||||
import {
|
||||
|
@ -28,12 +29,13 @@ import {
|
|||
EmbeddableOutput,
|
||||
IContainer,
|
||||
} from '../../../../../../src/plugins/embeddable/public';
|
||||
import { DOC_TYPE, Document } from '../../persistence';
|
||||
import { DOC_TYPE, Document, injectFilterReferences } from '../../persistence';
|
||||
import { ExpressionWrapper } from './expression_wrapper';
|
||||
import { UiActionsStart } from '../../../../../../src/plugins/ui_actions/public';
|
||||
import { isLensBrushEvent, isLensFilterEvent } from '../../types';
|
||||
|
||||
export interface LensEmbeddableConfiguration {
|
||||
expression: string | null;
|
||||
savedVis: Document;
|
||||
editUrl: string;
|
||||
editPath: string;
|
||||
|
@ -56,12 +58,13 @@ export class Embeddable extends AbstractEmbeddable<LensEmbeddableInput, LensEmbe
|
|||
|
||||
private expressionRenderer: ReactExpressionRendererType;
|
||||
private getTrigger: UiActionsStart['getTrigger'] | undefined;
|
||||
private expression: string | null;
|
||||
private savedVis: Document;
|
||||
private domNode: HTMLElement | Element | undefined;
|
||||
private subscription: Subscription;
|
||||
private autoRefreshFetchSubscription: Subscription;
|
||||
|
||||
private currentContext: {
|
||||
private externalSearchContext: {
|
||||
timeRange?: TimeRange;
|
||||
query?: Query;
|
||||
filters?: Filter[];
|
||||
|
@ -72,7 +75,14 @@ export class Embeddable extends AbstractEmbeddable<LensEmbeddableInput, LensEmbe
|
|||
timefilter: TimefilterContract,
|
||||
expressionRenderer: ReactExpressionRendererType,
|
||||
getTrigger: UiActionsStart['getTrigger'] | undefined,
|
||||
{ savedVis, editPath, editUrl, editable, indexPatterns }: LensEmbeddableConfiguration,
|
||||
{
|
||||
expression,
|
||||
savedVis,
|
||||
editPath,
|
||||
editUrl,
|
||||
editable,
|
||||
indexPatterns,
|
||||
}: LensEmbeddableConfiguration,
|
||||
initialInput: LensEmbeddableInput,
|
||||
parent?: IContainer
|
||||
) {
|
||||
|
@ -95,6 +105,7 @@ export class Embeddable extends AbstractEmbeddable<LensEmbeddableInput, LensEmbe
|
|||
|
||||
this.getTrigger = getTrigger;
|
||||
this.expressionRenderer = expressionRenderer;
|
||||
this.expression = expression;
|
||||
this.savedVis = savedVis;
|
||||
this.subscription = this.getInput$().subscribe((input) => this.onContainerStateChanged(input));
|
||||
this.onContainerStateChanged(initialInput);
|
||||
|
@ -122,14 +133,14 @@ export class Embeddable extends AbstractEmbeddable<LensEmbeddableInput, LensEmbe
|
|||
? containerState.filters.filter((filter) => !filter.meta.disabled)
|
||||
: undefined;
|
||||
if (
|
||||
!_.isEqual(containerState.timeRange, this.currentContext.timeRange) ||
|
||||
!_.isEqual(containerState.query, this.currentContext.query) ||
|
||||
!_.isEqual(cleanedFilters, this.currentContext.filters)
|
||||
!_.isEqual(containerState.timeRange, this.externalSearchContext.timeRange) ||
|
||||
!_.isEqual(containerState.query, this.externalSearchContext.query) ||
|
||||
!_.isEqual(cleanedFilters, this.externalSearchContext.filters)
|
||||
) {
|
||||
this.currentContext = {
|
||||
this.externalSearchContext = {
|
||||
timeRange: containerState.timeRange,
|
||||
query: containerState.query,
|
||||
lastReloadRequestTime: this.currentContext.lastReloadRequestTime,
|
||||
lastReloadRequestTime: this.externalSearchContext.lastReloadRequestTime,
|
||||
filters: cleanedFilters,
|
||||
};
|
||||
|
||||
|
@ -149,14 +160,37 @@ export class Embeddable extends AbstractEmbeddable<LensEmbeddableInput, LensEmbe
|
|||
render(
|
||||
<ExpressionWrapper
|
||||
ExpressionRenderer={this.expressionRenderer}
|
||||
expression={this.savedVis.expression}
|
||||
context={this.currentContext}
|
||||
expression={this.expression}
|
||||
searchContext={this.getMergedSearchContext()}
|
||||
handleEvent={this.handleEvent}
|
||||
/>,
|
||||
domNode
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Combines the embeddable context with the saved object context, and replaces
|
||||
* any references to index patterns
|
||||
*/
|
||||
private getMergedSearchContext(): ExecutionContextSearch {
|
||||
const output: ExecutionContextSearch = {
|
||||
timeRange: this.externalSearchContext.timeRange,
|
||||
};
|
||||
if (this.externalSearchContext.query) {
|
||||
output.query = [this.externalSearchContext.query, this.savedVis.state.query];
|
||||
} else {
|
||||
output.query = [this.savedVis.state.query];
|
||||
}
|
||||
if (this.externalSearchContext.filters?.length) {
|
||||
output.filters = [...this.externalSearchContext.filters, ...this.savedVis.state.filters];
|
||||
} else {
|
||||
output.filters = [...this.savedVis.state.filters];
|
||||
}
|
||||
|
||||
output.filters = injectFilterReferences(output.filters, this.savedVis.references);
|
||||
return output;
|
||||
}
|
||||
|
||||
handleEvent = (event: ExpressionRendererEvent) => {
|
||||
if (!this.getTrigger || this.input.disableTriggers) {
|
||||
return;
|
||||
|
@ -188,9 +222,9 @@ export class Embeddable extends AbstractEmbeddable<LensEmbeddableInput, LensEmbe
|
|||
|
||||
reload() {
|
||||
const currentTime = Date.now();
|
||||
if (this.currentContext.lastReloadRequestTime !== currentTime) {
|
||||
this.currentContext = {
|
||||
...this.currentContext,
|
||||
if (this.externalSearchContext.lastReloadRequestTime !== currentTime) {
|
||||
this.externalSearchContext = {
|
||||
...this.externalSearchContext,
|
||||
lastReloadRequestTime: currentTime,
|
||||
};
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
import { Capabilities, HttpSetup, SavedObjectsClientContract } from 'kibana/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { RecursiveReadonly } from '@kbn/utility-types';
|
||||
import { toExpression, Ast } from '@kbn/interpreter/target/common';
|
||||
import {
|
||||
IndexPatternsContract,
|
||||
IndexPattern,
|
||||
|
@ -23,6 +24,7 @@ import { Embeddable } from './embeddable';
|
|||
import { SavedObjectIndexStore, DOC_TYPE } from '../../persistence';
|
||||
import { getEditPath } from '../../../common';
|
||||
import { UiActionsStart } from '../../../../../../src/plugins/ui_actions/public';
|
||||
import { Document } from '../../persistence/saved_object_store';
|
||||
|
||||
interface StartServices {
|
||||
timefilter: TimefilterContract;
|
||||
|
@ -32,6 +34,7 @@ interface StartServices {
|
|||
expressionRenderer: ReactExpressionRendererType;
|
||||
indexPatternService: IndexPatternsContract;
|
||||
uiActions?: UiActionsStart;
|
||||
documentToExpression: (doc: Document) => Promise<Ast | null>;
|
||||
}
|
||||
|
||||
export class EmbeddableFactory implements EmbeddableFactoryDefinition {
|
||||
|
@ -72,13 +75,15 @@ export class EmbeddableFactory implements EmbeddableFactoryDefinition {
|
|||
indexPatternService,
|
||||
timefilter,
|
||||
expressionRenderer,
|
||||
documentToExpression,
|
||||
uiActions,
|
||||
} = await this.getStartServices();
|
||||
const store = new SavedObjectIndexStore(savedObjectsClient);
|
||||
const savedVis = await store.load(savedObjectId);
|
||||
|
||||
const promises = savedVis.state.datasourceMetaData.filterableIndexPatterns.map(
|
||||
async ({ id }) => {
|
||||
const promises = savedVis.references
|
||||
.filter(({ type }) => type === 'index-pattern')
|
||||
.map(async ({ id }) => {
|
||||
try {
|
||||
return await indexPatternService.get(id);
|
||||
} catch (error) {
|
||||
|
@ -87,14 +92,15 @@ export class EmbeddableFactory implements EmbeddableFactoryDefinition {
|
|||
// to show.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
const indexPatterns = (
|
||||
await Promise.all(promises)
|
||||
).filter((indexPattern: IndexPattern | null): indexPattern is IndexPattern =>
|
||||
Boolean(indexPattern)
|
||||
);
|
||||
|
||||
const expression = await documentToExpression(savedVis);
|
||||
|
||||
return new Embeddable(
|
||||
timefilter,
|
||||
expressionRenderer,
|
||||
|
@ -105,6 +111,7 @@ export class EmbeddableFactory implements EmbeddableFactoryDefinition {
|
|||
editUrl: coreHttp.basePath.prepend(`/app/lens${getEditPath(savedObjectId)}`),
|
||||
editable: await this.isEditable(),
|
||||
indexPatterns,
|
||||
expression: expression ? toExpression(expression) : null,
|
||||
},
|
||||
input,
|
||||
parent
|
||||
|
|
|
@ -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) => <div data-test-subj="expression-renderer-error">{error}</div>}
|
||||
onEvent={handleEvent}
|
||||
/>
|
||||
|
|
|
@ -31,7 +31,6 @@ export function createMockVisualization(): jest.Mocked<Visualization> {
|
|||
getVisualizationTypeId: jest.fn((_state) => 'empty'),
|
||||
getDescription: jest.fn((_state) => ({ label: '' })),
|
||||
switchVisualizationType: jest.fn((_, x) => x),
|
||||
getPersistableState: jest.fn((_state) => _state),
|
||||
getSuggestions: jest.fn((_options) => []),
|
||||
initialize: jest.fn((_frame, _state?) => ({})),
|
||||
getConfiguration: jest.fn((props) => ({
|
||||
|
@ -71,7 +70,7 @@ export function createMockDatasource(id: string): DatasourceMock {
|
|||
clearLayer: jest.fn((state, _layerId) => state),
|
||||
getDatasourceSuggestionsForField: jest.fn((_state, _item) => []),
|
||||
getDatasourceSuggestionsFromCurrentState: jest.fn((_state) => []),
|
||||
getPersistableState: jest.fn(),
|
||||
getPersistableState: jest.fn((x) => ({ state: x, savedObjectReferences: [] })),
|
||||
getPublicAPI: jest.fn().mockReturnValue(publicAPIMock),
|
||||
initialize: jest.fn((_state?) => Promise.resolve()),
|
||||
renderDataPanel: jest.fn(),
|
||||
|
@ -81,7 +80,6 @@ export function createMockDatasource(id: string): DatasourceMock {
|
|||
removeLayer: jest.fn((_state, _layerId) => {}),
|
||||
removeColumn: jest.fn((props) => {}),
|
||||
getLayers: jest.fn((_state) => []),
|
||||
getMetaData: jest.fn((_state) => ({ filterableIndexPatterns: [] })),
|
||||
|
||||
renderDimensionTrigger: jest.fn(),
|
||||
renderDimensionEditor: jest.fn(),
|
||||
|
|
|
@ -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<Datasource | Promise<Datasource>> = [];
|
||||
private readonly visualizations: Array<Visualization | Promise<Visualization>> = [];
|
||||
|
||||
/**
|
||||
* This method takes a Lens saved object as returned from the persistence helper,
|
||||
* initializes datsources and visualization and creates the current expression.
|
||||
* This is an asynchronous process and should only be triggered once for a saved object.
|
||||
* @param doc parsed Lens saved object
|
||||
*/
|
||||
private async documentToExpression(doc: Document) {
|
||||
const [resolvedDatasources, resolvedVisualizations] = await Promise.all([
|
||||
collectAsyncDefinitions(this.datasources),
|
||||
collectAsyncDefinitions(this.visualizations),
|
||||
]);
|
||||
|
||||
return await persistedStateToExpression(resolvedDatasources, resolvedVisualizations, doc);
|
||||
}
|
||||
|
||||
public setup(
|
||||
core: CoreSetup<EditorFrameStartPlugins>,
|
||||
plugins: EditorFrameSetupPlugins
|
||||
|
@ -74,6 +91,7 @@ export class EditorFrameService {
|
|||
coreHttp: coreStart.http,
|
||||
timefilter: deps.data.query.timefilter.timefilter,
|
||||
expressionRenderer: deps.expressions.ReactExpressionRenderer,
|
||||
documentToExpression: this.documentToExpression.bind(this),
|
||||
indexPatternService: deps.data.indexPatterns,
|
||||
uiActions: deps.uiActions,
|
||||
};
|
||||
|
@ -88,7 +106,7 @@ export class EditorFrameService {
|
|||
this.datasources.push(datasource as Datasource<unknown, unknown>);
|
||||
},
|
||||
registerVisualization: (visualization) => {
|
||||
this.visualizations.push(visualization as Visualization<unknown, unknown>);
|
||||
this.visualizations.push(visualization as Visualization<unknown>);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -23,3 +23,7 @@ export function loadInitialState() {
|
|||
};
|
||||
return result;
|
||||
}
|
||||
|
||||
const originalLoader = jest.requireActual('../loader');
|
||||
|
||||
export const extractReferences = originalLoader.extractReferences;
|
||||
|
|
|
@ -128,12 +128,15 @@ const expectedIndexPatterns = {
|
|||
},
|
||||
};
|
||||
|
||||
function stateFromPersistedState(
|
||||
persistedState: IndexPatternPersistedState
|
||||
): IndexPatternPrivateState {
|
||||
type IndexPatternBaseState = Omit<
|
||||
IndexPatternPrivateState,
|
||||
'indexPatternRefs' | 'indexPatterns' | 'existingFields' | 'isFirstExistenceFetch'
|
||||
>;
|
||||
|
||||
function enrichBaseState(baseState: IndexPatternBaseState): IndexPatternPrivateState {
|
||||
return {
|
||||
currentIndexPatternId: persistedState.currentIndexPatternId,
|
||||
layers: persistedState.layers,
|
||||
currentIndexPatternId: baseState.currentIndexPatternId,
|
||||
layers: baseState.layers,
|
||||
indexPatterns: expectedIndexPatterns,
|
||||
indexPatternRefs: [],
|
||||
existingFields: {},
|
||||
|
@ -142,7 +145,10 @@ function stateFromPersistedState(
|
|||
}
|
||||
|
||||
describe('IndexPattern Data Source', () => {
|
||||
let persistedState: IndexPatternPersistedState;
|
||||
let baseState: Omit<
|
||||
IndexPatternPrivateState,
|
||||
'indexPatternRefs' | 'indexPatterns' | 'existingFields' | 'isFirstExistenceFetch'
|
||||
>;
|
||||
let indexPatternDatasource: Datasource<IndexPatternPrivateState, IndexPatternPersistedState>;
|
||||
|
||||
beforeEach(() => {
|
||||
|
@ -153,7 +159,7 @@ describe('IndexPattern Data Source', () => {
|
|||
charts: chartPluginMock.createSetupContract(),
|
||||
});
|
||||
|
||||
persistedState = {
|
||||
baseState = {
|
||||
currentIndexPatternId: '1',
|
||||
layers: {
|
||||
first: {
|
||||
|
@ -224,9 +230,37 @@ describe('IndexPattern Data Source', () => {
|
|||
|
||||
describe('#getPersistedState', () => {
|
||||
it('should persist from saved state', async () => {
|
||||
const state = stateFromPersistedState(persistedState);
|
||||
const state = enrichBaseState(baseState);
|
||||
|
||||
expect(indexPatternDatasource.getPersistableState(state)).toEqual(persistedState);
|
||||
expect(indexPatternDatasource.getPersistableState(state)).toEqual({
|
||||
state: {
|
||||
layers: {
|
||||
first: {
|
||||
columnOrder: ['col1'],
|
||||
columns: {
|
||||
col1: {
|
||||
label: 'My Op',
|
||||
dataType: 'string',
|
||||
isBucketed: true,
|
||||
|
||||
// Private
|
||||
operationType: 'terms',
|
||||
sourceField: 'op',
|
||||
params: {
|
||||
size: 5,
|
||||
orderBy: { type: 'alphabetical' },
|
||||
orderDirection: 'asc',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
savedObjectReferences: [
|
||||
{ name: 'indexpattern-datasource-current-indexpattern', type: 'index-pattern', id: '1' },
|
||||
{ name: 'indexpattern-datasource-layer-first', type: 'index-pattern', id: '1' },
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -237,7 +271,7 @@ describe('IndexPattern Data Source', () => {
|
|||
});
|
||||
|
||||
it('should generate an expression for an aggregated query', async () => {
|
||||
const queryPersistedState: IndexPatternPersistedState = {
|
||||
const queryBaseState: IndexPatternBaseState = {
|
||||
currentIndexPatternId: '1',
|
||||
layers: {
|
||||
first: {
|
||||
|
@ -266,7 +300,7 @@ describe('IndexPattern Data Source', () => {
|
|||
},
|
||||
};
|
||||
|
||||
const state = stateFromPersistedState(queryPersistedState);
|
||||
const state = enrichBaseState(queryBaseState);
|
||||
|
||||
expect(indexPatternDatasource.toExpression(state, 'first')).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
|
@ -311,7 +345,7 @@ describe('IndexPattern Data Source', () => {
|
|||
});
|
||||
|
||||
it('should put all time fields used in date_histograms to the esaggs timeFields parameter', async () => {
|
||||
const queryPersistedState: IndexPatternPersistedState = {
|
||||
const queryBaseState: IndexPatternBaseState = {
|
||||
currentIndexPatternId: '1',
|
||||
layers: {
|
||||
first: {
|
||||
|
@ -350,14 +384,14 @@ describe('IndexPattern Data Source', () => {
|
|||
},
|
||||
};
|
||||
|
||||
const state = stateFromPersistedState(queryPersistedState);
|
||||
const state = enrichBaseState(queryBaseState);
|
||||
|
||||
const ast = indexPatternDatasource.toExpression(state, 'first') as Ast;
|
||||
expect(ast.chain[0].arguments.timeFields).toEqual(['timestamp', 'another_datefield']);
|
||||
});
|
||||
|
||||
it('should not put date fields used outside date_histograms to the esaggs timeFields parameter', async () => {
|
||||
const queryPersistedState: IndexPatternPersistedState = {
|
||||
const queryBaseState: IndexPatternBaseState = {
|
||||
currentIndexPatternId: '1',
|
||||
layers: {
|
||||
first: {
|
||||
|
@ -386,7 +420,7 @@ describe('IndexPattern Data Source', () => {
|
|||
},
|
||||
};
|
||||
|
||||
const state = stateFromPersistedState(queryPersistedState);
|
||||
const state = enrichBaseState(queryBaseState);
|
||||
|
||||
const ast = indexPatternDatasource.toExpression(state, 'first') as Ast;
|
||||
expect(ast.chain[0].arguments.timeFields).toEqual(['timestamp']);
|
||||
|
@ -489,55 +523,14 @@ describe('IndexPattern Data Source', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('#getMetadata', () => {
|
||||
it('should return the title of the index patterns', () => {
|
||||
expect(
|
||||
indexPatternDatasource.getMetaData({
|
||||
indexPatternRefs: [],
|
||||
existingFields: {},
|
||||
isFirstExistenceFetch: false,
|
||||
indexPatterns: expectedIndexPatterns,
|
||||
layers: {
|
||||
first: {
|
||||
indexPatternId: '1',
|
||||
columnOrder: [],
|
||||
columns: {},
|
||||
},
|
||||
second: {
|
||||
indexPatternId: '2',
|
||||
columnOrder: [],
|
||||
columns: {},
|
||||
},
|
||||
},
|
||||
currentIndexPatternId: '1',
|
||||
})
|
||||
).toEqual({
|
||||
filterableIndexPatterns: [
|
||||
{
|
||||
id: '1',
|
||||
title: 'my-fake-index-pattern',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: 'my-fake-restricted-pattern',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getPublicAPI', () => {
|
||||
let publicAPI: DatasourcePublicAPI;
|
||||
|
||||
beforeEach(async () => {
|
||||
const initialState = stateFromPersistedState(persistedState);
|
||||
const initialState = enrichBaseState(baseState);
|
||||
publicAPI = indexPatternDatasource.getPublicAPI({
|
||||
state: initialState,
|
||||
layerId: 'first',
|
||||
dateRange: {
|
||||
fromDate: 'now-30d',
|
||||
toDate: 'now',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ import _ from 'lodash';
|
|||
import React from 'react';
|
||||
import { render } from 'react-dom';
|
||||
import { I18nProvider } from '@kbn/i18n/react';
|
||||
import { CoreStart } from 'kibana/public';
|
||||
import { CoreStart, SavedObjectReference } from 'kibana/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { IStorageWrapper } from 'src/plugins/kibana_utils/public';
|
||||
import {
|
||||
|
@ -19,7 +19,12 @@ import {
|
|||
DatasourceLayerPanelProps,
|
||||
PublicAPIProps,
|
||||
} from '../types';
|
||||
import { loadInitialState, changeIndexPattern, changeLayerIndexPattern } from './loader';
|
||||
import {
|
||||
loadInitialState,
|
||||
changeIndexPattern,
|
||||
changeLayerIndexPattern,
|
||||
extractReferences,
|
||||
} from './loader';
|
||||
import { toExpression } from './to_expression';
|
||||
import {
|
||||
IndexPatternDimensionTrigger,
|
||||
|
@ -125,9 +130,13 @@ export function getIndexPatternDatasource({
|
|||
const indexPatternDatasource: Datasource<IndexPatternPrivateState, IndexPatternPersistedState> = {
|
||||
id: 'indexpattern',
|
||||
|
||||
async initialize(state?: IndexPatternPersistedState) {
|
||||
async initialize(
|
||||
persistedState?: IndexPatternPersistedState,
|
||||
references?: SavedObjectReference[]
|
||||
) {
|
||||
return loadInitialState({
|
||||
state,
|
||||
persistedState,
|
||||
references,
|
||||
savedObjectsClient: await savedObjectsClient,
|
||||
defaultIndexPatternId: core.uiSettings.get('defaultIndex'),
|
||||
storage,
|
||||
|
@ -135,8 +144,8 @@ export function getIndexPatternDatasource({
|
|||
});
|
||||
},
|
||||
|
||||
getPersistableState({ currentIndexPatternId, layers }: IndexPatternPrivateState) {
|
||||
return { currentIndexPatternId, layers };
|
||||
getPersistableState(state: IndexPatternPrivateState) {
|
||||
return extractReferences(state);
|
||||
},
|
||||
|
||||
insertLayer(state: IndexPatternPrivateState, newLayerId: string) {
|
||||
|
@ -183,19 +192,6 @@ export function getIndexPatternDatasource({
|
|||
|
||||
toExpression,
|
||||
|
||||
getMetaData(state: IndexPatternPrivateState) {
|
||||
return {
|
||||
filterableIndexPatterns: _.uniq(
|
||||
Object.values(state.layers)
|
||||
.map((layer) => layer.indexPatternId)
|
||||
.map((indexPatternId) => ({
|
||||
id: indexPatternId,
|
||||
title: state.indexPatterns[indexPatternId].title,
|
||||
}))
|
||||
),
|
||||
};
|
||||
},
|
||||
|
||||
renderDataPanel(
|
||||
domElement: Element,
|
||||
props: DatasourceDataPanelProps<IndexPatternPrivateState>
|
||||
|
|
|
@ -12,6 +12,8 @@ import {
|
|||
changeIndexPattern,
|
||||
changeLayerIndexPattern,
|
||||
syncExistingFields,
|
||||
extractReferences,
|
||||
injectReferences,
|
||||
} from './loader';
|
||||
import { IndexPatternsContract } from '../../../../../src/plugins/data/public';
|
||||
import {
|
||||
|
@ -378,10 +380,8 @@ describe('loader', () => {
|
|||
|
||||
it('should initialize from saved state', async () => {
|
||||
const savedState: IndexPatternPersistedState = {
|
||||
currentIndexPatternId: '2',
|
||||
layers: {
|
||||
layerb: {
|
||||
indexPatternId: '2',
|
||||
columnOrder: ['col1', 'col2'],
|
||||
columns: {
|
||||
col1: {
|
||||
|
@ -407,7 +407,12 @@ describe('loader', () => {
|
|||
};
|
||||
const storage = createMockStorage({ indexPatternId: '1' });
|
||||
const state = await loadInitialState({
|
||||
state: savedState,
|
||||
persistedState: savedState,
|
||||
references: [
|
||||
{ name: 'indexpattern-datasource-current-indexpattern', id: '2', type: 'index-pattern' },
|
||||
{ name: 'indexpattern-datasource-layer-layerb', id: '2', type: 'index-pattern' },
|
||||
{ name: 'another-reference', id: 'c', type: 'index-pattern' },
|
||||
],
|
||||
savedObjectsClient: mockClient(),
|
||||
indexPatternsService: mockIndexPatternsService(),
|
||||
storage,
|
||||
|
@ -422,7 +427,7 @@ describe('loader', () => {
|
|||
indexPatterns: {
|
||||
'2': sampleIndexPatterns['2'],
|
||||
},
|
||||
layers: savedState.layers,
|
||||
layers: { layerb: { ...savedState.layers.layerb, indexPatternId: '2' } },
|
||||
});
|
||||
|
||||
expect(storage.set).toHaveBeenCalledWith('lens-settings', {
|
||||
|
@ -431,6 +436,79 @@ describe('loader', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('saved object references', () => {
|
||||
const state: IndexPatternPrivateState = {
|
||||
currentIndexPatternId: 'b',
|
||||
indexPatternRefs: [],
|
||||
indexPatterns: {},
|
||||
existingFields: {},
|
||||
layers: {
|
||||
a: {
|
||||
indexPatternId: 'id-index-pattern-a',
|
||||
columnOrder: ['col1'],
|
||||
columns: {
|
||||
col1: {
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
label: '',
|
||||
operationType: 'avg',
|
||||
sourceField: 'myfield',
|
||||
},
|
||||
},
|
||||
},
|
||||
b: {
|
||||
indexPatternId: 'id-index-pattern-b',
|
||||
columnOrder: ['col2'],
|
||||
columns: {
|
||||
col2: {
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
label: '',
|
||||
operationType: 'avg',
|
||||
sourceField: 'myfield2',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
isFirstExistenceFetch: false,
|
||||
};
|
||||
|
||||
it('should create a reference for each layer and for current index pattern', () => {
|
||||
const { savedObjectReferences } = extractReferences(state);
|
||||
expect(savedObjectReferences).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"id": "b",
|
||||
"name": "indexpattern-datasource-current-indexpattern",
|
||||
"type": "index-pattern",
|
||||
},
|
||||
Object {
|
||||
"id": "id-index-pattern-a",
|
||||
"name": "indexpattern-datasource-layer-a",
|
||||
"type": "index-pattern",
|
||||
},
|
||||
Object {
|
||||
"id": "id-index-pattern-b",
|
||||
"name": "indexpattern-datasource-layer-b",
|
||||
"type": "index-pattern",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('should restore layers', () => {
|
||||
const { savedObjectReferences, state: persistedState } = extractReferences(state);
|
||||
expect(injectReferences(persistedState, savedObjectReferences).layers).toEqual(state.layers);
|
||||
});
|
||||
|
||||
it('should restore current index pattern', () => {
|
||||
const { savedObjectReferences, state: persistedState } = extractReferences(state);
|
||||
expect(injectReferences(persistedState, savedObjectReferences).currentIndexPatternId).toEqual(
|
||||
state.currentIndexPatternId
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('changeIndexPattern', () => {
|
||||
it('loads the index pattern and then sets it as current', async () => {
|
||||
const setState = jest.fn();
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import _ from 'lodash';
|
||||
import { IStorageWrapper } from 'src/plugins/kibana_utils/public';
|
||||
import { SavedObjectsClientContract, HttpSetup } from 'kibana/public';
|
||||
import { SavedObjectsClientContract, HttpSetup, SavedObjectReference } from 'kibana/public';
|
||||
import { StateSetter } from '../types';
|
||||
import {
|
||||
IndexPattern,
|
||||
|
@ -14,6 +14,7 @@ import {
|
|||
IndexPatternPersistedState,
|
||||
IndexPatternPrivateState,
|
||||
IndexPatternField,
|
||||
IndexPatternLayer,
|
||||
} from './types';
|
||||
import { updateLayerIndexPattern } from './state_helpers';
|
||||
import { DateRange, ExistingFields } from '../../common/types';
|
||||
|
@ -115,14 +116,58 @@ const setLastUsedIndexPatternId = (storage: IStorageWrapper, value: string) => {
|
|||
writeToStorage(storage, 'indexPatternId', value);
|
||||
};
|
||||
|
||||
const CURRENT_PATTERN_REFERENCE_NAME = 'indexpattern-datasource-current-indexpattern';
|
||||
function getLayerReferenceName(layerId: string) {
|
||||
return `indexpattern-datasource-layer-${layerId}`;
|
||||
}
|
||||
|
||||
export function extractReferences({ currentIndexPatternId, layers }: IndexPatternPrivateState) {
|
||||
const savedObjectReferences: SavedObjectReference[] = [];
|
||||
savedObjectReferences.push({
|
||||
type: 'index-pattern',
|
||||
id: currentIndexPatternId,
|
||||
name: CURRENT_PATTERN_REFERENCE_NAME,
|
||||
});
|
||||
const persistableLayers: Record<string, Omit<IndexPatternLayer, 'indexPatternId'>> = {};
|
||||
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<string, IndexPatternLayer> = {};
|
||||
Object.entries(state.layers).forEach(([layerId, persistedLayer]) => {
|
||||
layers[layerId] = {
|
||||
...persistedLayer,
|
||||
indexPatternId: references.find(({ name }) => name === getLayerReferenceName(layerId))!.id,
|
||||
};
|
||||
});
|
||||
return {
|
||||
currentIndexPatternId: references.find(({ name }) => name === CURRENT_PATTERN_REFERENCE_NAME)!
|
||||
.id,
|
||||
layers,
|
||||
};
|
||||
}
|
||||
|
||||
export async function loadInitialState({
|
||||
state,
|
||||
persistedState,
|
||||
references,
|
||||
savedObjectsClient,
|
||||
defaultIndexPatternId,
|
||||
storage,
|
||||
indexPatternsService,
|
||||
}: {
|
||||
state?: IndexPatternPersistedState;
|
||||
persistedState?: IndexPatternPersistedState;
|
||||
references?: SavedObjectReference[];
|
||||
savedObjectsClient: SavedObjectsClient;
|
||||
defaultIndexPatternId?: string;
|
||||
storage: IStorageWrapper;
|
||||
|
@ -131,6 +176,9 @@ export async function loadInitialState({
|
|||
const indexPatternRefs = await loadIndexPatternRefs(savedObjectsClient);
|
||||
const lastUsedIndexPatternId = getLastUsedIndexPatternId(storage, indexPatternRefs);
|
||||
|
||||
const state =
|
||||
persistedState && references ? injectReferences(persistedState, references) : undefined;
|
||||
|
||||
const requiredPatterns = _.uniq(
|
||||
state
|
||||
? Object.values(state.layers)
|
||||
|
|
|
@ -40,11 +40,12 @@ export interface IndexPatternLayer {
|
|||
}
|
||||
|
||||
export interface IndexPatternPersistedState {
|
||||
currentIndexPatternId: string;
|
||||
layers: Record<string, IndexPatternLayer>;
|
||||
layers: Record<string, Omit<IndexPatternLayer, 'indexPatternId'>>;
|
||||
}
|
||||
|
||||
export type IndexPatternPrivateState = IndexPatternPersistedState & {
|
||||
export interface IndexPatternPrivateState {
|
||||
currentIndexPatternId: string;
|
||||
layers: Record<string, IndexPatternLayer>;
|
||||
indexPatternRefs: IndexPatternRef[];
|
||||
indexPatterns: Record<string, IndexPattern>;
|
||||
|
||||
|
@ -54,7 +55,7 @@ export type IndexPatternPrivateState = IndexPatternPersistedState & {
|
|||
existingFields: Record<string, Record<string, boolean>>;
|
||||
isFirstExistenceFetch: boolean;
|
||||
existenceFetchFailed?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IndexPatternRef {
|
||||
id: string;
|
||||
|
|
|
@ -66,12 +66,6 @@ describe('metric_visualization', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('#getPersistableState', () => {
|
||||
it('persists the state as given', () => {
|
||||
expect(metricVisualization.getPersistableState(exampleState())).toEqual(exampleState());
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getConfiguration', () => {
|
||||
it('can add a metric when there is no accessor', () => {
|
||||
expect(
|
||||
|
@ -168,7 +162,8 @@ describe('metric_visualization', () => {
|
|||
datasourceLayers: { l1: datasource },
|
||||
};
|
||||
|
||||
expect(metricVisualization.toExpression(exampleState(), frame)).toMatchInlineSnapshot(`
|
||||
expect(metricVisualization.toExpression(exampleState(), frame.datasourceLayers))
|
||||
.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"chain": Array [
|
||||
Object {
|
||||
|
|
|
@ -7,20 +7,20 @@
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { Ast } from '@kbn/interpreter/target/common';
|
||||
import { getSuggestions } from './metric_suggestions';
|
||||
import { Visualization, FramePublicAPI, OperationMetadata } from '../types';
|
||||
import { State, PersistableState } from './types';
|
||||
import { Visualization, OperationMetadata, DatasourcePublicAPI } from '../types';
|
||||
import { State } from './types';
|
||||
import chartMetricSVG from '../assets/chart_metric.svg';
|
||||
|
||||
const toExpression = (
|
||||
state: State,
|
||||
frame: FramePublicAPI,
|
||||
datasourceLayers: Record<string, DatasourcePublicAPI>,
|
||||
mode: 'reduced' | 'full' = 'full'
|
||||
): Ast | null => {
|
||||
if (!state.accessor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [datasource] = Object.values(frame.datasourceLayers);
|
||||
const [datasource] = Object.values(datasourceLayers);
|
||||
const operation = datasource && datasource.getOperationForColumnId(state.accessor);
|
||||
|
||||
return {
|
||||
|
@ -39,7 +39,7 @@ const toExpression = (
|
|||
};
|
||||
};
|
||||
|
||||
export const metricVisualization: Visualization<State, PersistableState> = {
|
||||
export const metricVisualization: Visualization<State> = {
|
||||
id: 'lnsMetric',
|
||||
|
||||
visualizationTypes: [
|
||||
|
@ -88,8 +88,6 @@ export const metricVisualization: Visualization<State, PersistableState> = {
|
|||
);
|
||||
},
|
||||
|
||||
getPersistableState: (state) => state,
|
||||
|
||||
getConfiguration(props) {
|
||||
return {
|
||||
groups: [
|
||||
|
@ -106,8 +104,8 @@ export const metricVisualization: Visualization<State, PersistableState> = {
|
|||
},
|
||||
|
||||
toExpression,
|
||||
toPreviewExpression: (state: State, frame: FramePublicAPI) =>
|
||||
toExpression(state, frame, 'reduced'),
|
||||
toPreviewExpression: (state, datasourceLayers) =>
|
||||
toExpression(state, datasourceLayers, 'reduced'),
|
||||
|
||||
setDimension({ prevState, columnId }) {
|
||||
return { ...prevState, accessor: columnId };
|
||||
|
|
|
@ -13,5 +13,3 @@ export interface MetricConfig extends State {
|
|||
title: string;
|
||||
mode: 'reduced' | 'full';
|
||||
}
|
||||
|
||||
export type PersistableState = State;
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
56
x-pack/plugins/lens/public/persistence/filter_references.ts
Normal file
56
x-pack/plugins/lens/public/persistence/filter_references.ts
Normal file
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Filter } from 'src/plugins/data/public';
|
||||
import { SavedObjectReference } from 'kibana/public';
|
||||
import { PersistableFilter } from '../../common';
|
||||
|
||||
export function extractFilterReferences(
|
||||
filters: Filter[]
|
||||
): { persistableFilters: PersistableFilter[]; references: SavedObjectReference[] } {
|
||||
const references: SavedObjectReference[] = [];
|
||||
const persistableFilters = filters.map((filterRow, i) => {
|
||||
if (!filterRow.meta || !filterRow.meta.index) {
|
||||
return filterRow;
|
||||
}
|
||||
const refName = `filter-index-pattern-${i}`;
|
||||
references.push({
|
||||
name: refName,
|
||||
type: 'index-pattern',
|
||||
id: filterRow.meta.index,
|
||||
});
|
||||
return {
|
||||
...filterRow,
|
||||
meta: {
|
||||
...filterRow.meta,
|
||||
indexRefName: refName,
|
||||
index: undefined,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return { persistableFilters, references };
|
||||
}
|
||||
|
||||
export function injectFilterReferences(
|
||||
filters: PersistableFilter[],
|
||||
references: SavedObjectReference[]
|
||||
) {
|
||||
return filters.map((filterRow) => {
|
||||
if (!filterRow.meta || !filterRow.meta.indexRefName) {
|
||||
return filterRow as Filter;
|
||||
}
|
||||
const { indexRefName, ...metaRest } = filterRow.meta;
|
||||
const reference = references.find((ref) => ref.name === indexRefName);
|
||||
if (!reference) {
|
||||
throw new Error(`Could not find reference for ${indexRefName}`);
|
||||
}
|
||||
return {
|
||||
...filterRow,
|
||||
meta: { ...metaRest, index: reference.id },
|
||||
};
|
||||
});
|
||||
}
|
|
@ -5,3 +5,4 @@
|
|||
*/
|
||||
|
||||
export * from './saved_object_store';
|
||||
export * from './filter_references';
|
||||
|
|
|
@ -30,11 +30,8 @@ describe('LensStore', () => {
|
|||
title: 'Hello',
|
||||
description: 'My doc',
|
||||
visualizationType: 'bar',
|
||||
expression: '',
|
||||
references: [],
|
||||
state: {
|
||||
datasourceMetaData: {
|
||||
filterableIndexPatterns: [],
|
||||
},
|
||||
datasourceStates: {
|
||||
indexpattern: { type: 'index_pattern', indexPattern: '.kibana_test' },
|
||||
},
|
||||
|
@ -49,11 +46,8 @@ describe('LensStore', () => {
|
|||
title: 'Hello',
|
||||
description: 'My doc',
|
||||
visualizationType: 'bar',
|
||||
expression: '',
|
||||
references: [],
|
||||
state: {
|
||||
datasourceMetaData: {
|
||||
filterableIndexPatterns: [],
|
||||
},
|
||||
datasourceStates: {
|
||||
indexpattern: { type: 'index_pattern', indexPattern: '.kibana_test' },
|
||||
},
|
||||
|
@ -64,21 +58,25 @@ describe('LensStore', () => {
|
|||
});
|
||||
|
||||
expect(client.create).toHaveBeenCalledTimes(1);
|
||||
expect(client.create).toHaveBeenCalledWith('lens', {
|
||||
title: 'Hello',
|
||||
description: 'My doc',
|
||||
visualizationType: 'bar',
|
||||
expression: '',
|
||||
state: {
|
||||
datasourceMetaData: { filterableIndexPatterns: [] },
|
||||
datasourceStates: {
|
||||
indexpattern: { type: 'index_pattern', indexPattern: '.kibana_test' },
|
||||
expect(client.create).toHaveBeenCalledWith(
|
||||
'lens',
|
||||
{
|
||||
title: 'Hello',
|
||||
description: 'My doc',
|
||||
visualizationType: 'bar',
|
||||
state: {
|
||||
datasourceStates: {
|
||||
indexpattern: { type: 'index_pattern', indexPattern: '.kibana_test' },
|
||||
},
|
||||
visualization: { x: 'foo', y: 'baz' },
|
||||
query: { query: '', language: 'lucene' },
|
||||
filters: [],
|
||||
},
|
||||
visualization: { x: 'foo', y: 'baz' },
|
||||
query: { query: '', language: 'lucene' },
|
||||
filters: [],
|
||||
},
|
||||
});
|
||||
{
|
||||
references: [],
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test('updates and returns a visualization document', async () => {
|
||||
|
@ -87,9 +85,8 @@ describe('LensStore', () => {
|
|||
id: 'Gandalf',
|
||||
title: 'Even the very wise cannot see all ends.',
|
||||
visualizationType: 'line',
|
||||
expression: '',
|
||||
references: [],
|
||||
state: {
|
||||
datasourceMetaData: { filterableIndexPatterns: [] },
|
||||
datasourceStates: { indexpattern: { type: 'index_pattern', indexPattern: 'lotr' } },
|
||||
visualization: { gear: ['staff', 'pointy hat'] },
|
||||
query: { query: '', language: 'lucene' },
|
||||
|
@ -101,9 +98,8 @@ describe('LensStore', () => {
|
|||
id: 'Gandalf',
|
||||
title: 'Even the very wise cannot see all ends.',
|
||||
visualizationType: 'line',
|
||||
expression: '',
|
||||
references: [],
|
||||
state: {
|
||||
datasourceMetaData: { filterableIndexPatterns: [] },
|
||||
datasourceStates: { indexpattern: { type: 'index_pattern', indexPattern: 'lotr' } },
|
||||
visualization: { gear: ['staff', 'pointy hat'] },
|
||||
query: { query: '', language: 'lucene' },
|
||||
|
@ -116,22 +112,21 @@ describe('LensStore', () => {
|
|||
{
|
||||
type: 'lens',
|
||||
id: 'Gandalf',
|
||||
references: [],
|
||||
attributes: {
|
||||
title: null,
|
||||
visualizationType: null,
|
||||
expression: null,
|
||||
state: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'lens',
|
||||
id: 'Gandalf',
|
||||
references: [],
|
||||
attributes: {
|
||||
title: 'Even the very wise cannot see all ends.',
|
||||
visualizationType: 'line',
|
||||
expression: '',
|
||||
state: {
|
||||
datasourceMetaData: { filterableIndexPatterns: [] },
|
||||
datasourceStates: { indexpattern: { type: 'index_pattern', indexPattern: 'lotr' } },
|
||||
visualization: { gear: ['staff', 'pointy hat'] },
|
||||
query: { query: '', language: 'lucene' },
|
||||
|
|
|
@ -4,8 +4,13 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { SavedObjectAttributes, SavedObjectsClientContract } from 'kibana/public';
|
||||
import { Query, Filter } from '../../../../../src/plugins/data/public';
|
||||
import {
|
||||
SavedObjectAttributes,
|
||||
SavedObjectsClientContract,
|
||||
SavedObjectReference,
|
||||
} from 'kibana/public';
|
||||
import { Query } from '../../../../../src/plugins/data/public';
|
||||
import { PersistableFilter } from '../../common';
|
||||
|
||||
export interface Document {
|
||||
id?: string;
|
||||
|
@ -13,16 +18,13 @@ export interface Document {
|
|||
visualizationType: string | null;
|
||||
title: string;
|
||||
description?: string;
|
||||
expression: string | null;
|
||||
state: {
|
||||
datasourceMetaData: {
|
||||
filterableIndexPatterns: Array<{ id: string; title: string }>;
|
||||
};
|
||||
datasourceStates: Record<string, unknown>;
|
||||
visualization: unknown;
|
||||
query: Query;
|
||||
filters: Filter[];
|
||||
filters: PersistableFilter[];
|
||||
};
|
||||
references: SavedObjectReference[];
|
||||
}
|
||||
|
||||
export const DOC_TYPE = 'lens';
|
||||
|
@ -45,14 +47,16 @@ export class SavedObjectIndexStore implements SavedObjectStore {
|
|||
}
|
||||
|
||||
async save(vis: Document) {
|
||||
const { id, type, ...rest } = vis;
|
||||
const { id, type, references, ...rest } = vis;
|
||||
// TODO: SavedObjectAttributes should support this kind of object,
|
||||
// remove this workaround when SavedObjectAttributes is updated.
|
||||
const attributes = (rest as unknown) as SavedObjectAttributes;
|
||||
|
||||
const result = await (id
|
||||
? this.safeUpdate(id, attributes)
|
||||
: this.client.create(DOC_TYPE, attributes));
|
||||
? this.safeUpdate(id, attributes, references)
|
||||
: this.client.create(DOC_TYPE, attributes, {
|
||||
references,
|
||||
}));
|
||||
|
||||
return { ...vis, id: result.id };
|
||||
}
|
||||
|
@ -63,21 +67,25 @@ export class SavedObjectIndexStore implements SavedObjectStore {
|
|||
// deleted subtrees make it back into the object after a load.
|
||||
// This function fixes this by doing two updates - one to empty out the document setting
|
||||
// every key to null, and a second one to load the new content.
|
||||
private async safeUpdate(id: string, attributes: SavedObjectAttributes) {
|
||||
private async safeUpdate(
|
||||
id: string,
|
||||
attributes: SavedObjectAttributes,
|
||||
references: SavedObjectReference[]
|
||||
) {
|
||||
const resetAttributes: SavedObjectAttributes = {};
|
||||
Object.keys(attributes).forEach((key) => {
|
||||
resetAttributes[key] = null;
|
||||
});
|
||||
return (
|
||||
await this.client.bulkUpdate([
|
||||
{ type: DOC_TYPE, id, attributes: resetAttributes },
|
||||
{ type: DOC_TYPE, id, attributes },
|
||||
{ type: DOC_TYPE, id, attributes: resetAttributes, references },
|
||||
{ type: DOC_TYPE, id, attributes, references },
|
||||
])
|
||||
).savedObjects[1];
|
||||
}
|
||||
|
||||
async load(id: string): Promise<Document> {
|
||||
const { type, attributes, error } = await this.client.get(DOC_TYPE, id);
|
||||
const { type, attributes, references, error } = await this.client.get(DOC_TYPE, id);
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
|
@ -85,6 +93,7 @@ export class SavedObjectIndexStore implements SavedObjectStore {
|
|||
|
||||
return {
|
||||
...(attributes as SavedObjectAttributes),
|
||||
references,
|
||||
id,
|
||||
type,
|
||||
} as Document;
|
||||
|
|
|
@ -31,7 +31,7 @@ const bucketedOperations = (op: OperationMetadata) => op.isBucketed;
|
|||
const numberMetricOperations = (op: OperationMetadata) =>
|
||||
!op.isBucketed && op.dataType === 'number';
|
||||
|
||||
export const pieVisualization: Visualization<PieVisualizationState, PieVisualizationState> = {
|
||||
export const pieVisualization: Visualization<PieVisualizationState> = {
|
||||
id: 'lnsPie',
|
||||
|
||||
visualizationTypes: [
|
||||
|
@ -91,8 +91,6 @@ export const pieVisualization: Visualization<PieVisualizationState, PieVisualiza
|
|||
);
|
||||
},
|
||||
|
||||
getPersistableState: (state) => state,
|
||||
|
||||
getSuggestions: suggestions,
|
||||
|
||||
getConfiguration({ state, frame, layerId }) {
|
||||
|
|
|
@ -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<string, DatasourcePublicAPI>
|
||||
) {
|
||||
return expressionHelper(state, datasourceLayers, false);
|
||||
}
|
||||
|
||||
function expressionHelper(
|
||||
state: PieVisualizationState,
|
||||
frame: FramePublicAPI,
|
||||
datasourceLayers: Record<string, DatasourcePublicAPI>,
|
||||
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<string, DatasourcePublicAPI>
|
||||
) {
|
||||
return expressionHelper(state, datasourceLayers, true);
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
import { Ast } from '@kbn/interpreter/common';
|
||||
import { IconType } from '@elastic/eui/src/components/icon/icon';
|
||||
import { CoreSetup } from 'kibana/public';
|
||||
import { SavedObjectReference } from 'kibana/public';
|
||||
import {
|
||||
ExpressionRendererEvent,
|
||||
IInterpreterRenderHandlers,
|
||||
|
@ -30,7 +31,6 @@ export type FormatFactory = (mapping?: SerializedFieldFormat) => IFieldFormat;
|
|||
export interface PublicAPIProps<T> {
|
||||
state: T;
|
||||
layerId: string;
|
||||
dateRange: DateRange;
|
||||
}
|
||||
|
||||
export interface EditorFrameProps {
|
||||
|
@ -44,8 +44,9 @@ export interface EditorFrameProps {
|
|||
// Frame loader (app or embeddable) is expected to call this when it loads and updates
|
||||
// This should be replaced with a top-down state
|
||||
onChange: (newState: {
|
||||
filterableIndexPatterns: DatasourceMetaData['filterableIndexPatterns'];
|
||||
filterableIndexPatterns: string[];
|
||||
doc: Document;
|
||||
isSaveable: boolean;
|
||||
}) => void;
|
||||
showNoDataPopover: () => void;
|
||||
}
|
||||
|
@ -57,9 +58,7 @@ export interface EditorFrameInstance {
|
|||
export interface EditorFrameSetup {
|
||||
// generic type on the API functions to pull the "unknown vs. specific type" error into the implementation
|
||||
registerDatasource: <T, P>(datasource: Datasource<T, P> | Promise<Datasource<T, P>>) => void;
|
||||
registerVisualization: <T, P>(
|
||||
visualization: Visualization<T, P> | Promise<Visualization<T, P>>
|
||||
) => void;
|
||||
registerVisualization: <T>(visualization: Visualization<T> | Promise<Visualization<T>>) => void;
|
||||
}
|
||||
|
||||
export interface EditorFrameStart {
|
||||
|
@ -131,10 +130,6 @@ export interface DatasourceSuggestion<T = unknown> {
|
|||
keptLayerIds: string[];
|
||||
}
|
||||
|
||||
export interface DatasourceMetaData {
|
||||
filterableIndexPatterns: Array<{ id: string; title: string }>;
|
||||
}
|
||||
|
||||
export type StateSetter<T> = (newState: T | ((prevState: T) => T)) => void;
|
||||
|
||||
/**
|
||||
|
@ -146,10 +141,10 @@ export interface Datasource<T = unknown, P = unknown> {
|
|||
// 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<T>;
|
||||
initialize: (state?: P, savedObjectReferences?: SavedObjectReference[]) => Promise<T>;
|
||||
|
||||
// Given the current state, which parts should be saved?
|
||||
getPersistableState: (state: T) => P;
|
||||
getPersistableState: (state: T) => { state: P; savedObjectReferences: SavedObjectReference[] };
|
||||
|
||||
insertLayer: (state: T, newLayerId: string) => T;
|
||||
removeLayer: (state: T, layerId: string) => T;
|
||||
|
@ -166,8 +161,6 @@ export interface Datasource<T = unknown, P = unknown> {
|
|||
|
||||
toExpression: (state: T, layerId: string) => Ast | string | null;
|
||||
|
||||
getMetaData: (state: T) => DatasourceMetaData;
|
||||
|
||||
getDatasourceSuggestionsForField: (state: T, field: unknown) => Array<DatasourceSuggestion<T>>;
|
||||
getDatasourceSuggestionsFromCurrentState: (state: T) => Array<DatasourceSuggestion<T>>;
|
||||
|
||||
|
@ -408,7 +401,7 @@ export interface VisualizationType {
|
|||
label: string;
|
||||
}
|
||||
|
||||
export interface Visualization<T = unknown, P = unknown> {
|
||||
export interface Visualization<T = unknown> {
|
||||
/** Plugin ID, such as "lnsXY" */
|
||||
id: string;
|
||||
|
||||
|
@ -418,11 +411,7 @@ export interface Visualization<T = unknown, P = unknown> {
|
|||
* - Loadingn from a saved visualization
|
||||
* - When using suggestions, the suggested state is passed in
|
||||
*/
|
||||
initialize: (frame: FramePublicAPI, state?: P) => T;
|
||||
/**
|
||||
* Can remove any state that should not be persisted to saved object, such as UI state
|
||||
*/
|
||||
getPersistableState: (state: T) => P;
|
||||
initialize: (frame: FramePublicAPI, state?: T) => T;
|
||||
|
||||
/**
|
||||
* Visualizations must provide at least one type for the chart switcher,
|
||||
|
@ -504,12 +493,18 @@ export interface Visualization<T = unknown, P = unknown> {
|
|||
*/
|
||||
getSuggestions: (context: SuggestionRequest<T>) => Array<VisualizationSuggestion<T>>;
|
||||
|
||||
toExpression: (state: T, frame: FramePublicAPI) => Ast | string | null;
|
||||
toExpression: (
|
||||
state: T,
|
||||
datasourceLayers: Record<string, DatasourcePublicAPI>
|
||||
) => 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<string, DatasourcePublicAPI>
|
||||
) => Ast | string | null;
|
||||
}
|
||||
|
||||
export interface LensFilterEvent {
|
||||
|
|
|
@ -53,7 +53,7 @@ describe('#toExpression', () => {
|
|||
},
|
||||
],
|
||||
},
|
||||
frame
|
||||
frame.datasourceLayers
|
||||
)
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
|
@ -74,7 +74,7 @@ describe('#toExpression', () => {
|
|||
},
|
||||
],
|
||||
},
|
||||
frame
|
||||
frame.datasourceLayers
|
||||
) as Ast).chain[0].arguments.fittingFunction[0]
|
||||
).toEqual('None');
|
||||
});
|
||||
|
@ -94,7 +94,7 @@ describe('#toExpression', () => {
|
|||
},
|
||||
],
|
||||
},
|
||||
frame
|
||||
frame.datasourceLayers
|
||||
) as Ast;
|
||||
expect(expression.chain[0].arguments.showXAxisTitle[0]).toBe(true);
|
||||
expect(expression.chain[0].arguments.showYAxisTitle[0]).toBe(true);
|
||||
|
@ -116,7 +116,7 @@ describe('#toExpression', () => {
|
|||
},
|
||||
],
|
||||
},
|
||||
frame
|
||||
frame.datasourceLayers
|
||||
)
|
||||
).toBeNull();
|
||||
});
|
||||
|
@ -137,7 +137,7 @@ describe('#toExpression', () => {
|
|||
},
|
||||
],
|
||||
},
|
||||
frame
|
||||
frame.datasourceLayers
|
||||
)
|
||||
).toBeNull();
|
||||
});
|
||||
|
@ -157,7 +157,7 @@ describe('#toExpression', () => {
|
|||
},
|
||||
],
|
||||
},
|
||||
frame
|
||||
frame.datasourceLayers
|
||||
)! as Ast;
|
||||
|
||||
expect(mockDatasource.publicAPIMock.getOperationForColumnId).toHaveBeenCalledWith('b');
|
||||
|
@ -191,7 +191,7 @@ describe('#toExpression', () => {
|
|||
},
|
||||
],
|
||||
},
|
||||
frame
|
||||
frame.datasourceLayers
|
||||
) as Ast;
|
||||
expect(
|
||||
(expression.chain[0].arguments.tickLabelsVisibilitySettings[0] as Ast).chain[0].arguments
|
||||
|
@ -216,7 +216,7 @@ describe('#toExpression', () => {
|
|||
},
|
||||
],
|
||||
},
|
||||
frame
|
||||
frame.datasourceLayers
|
||||
) as Ast;
|
||||
expect(
|
||||
(expression.chain[0].arguments.gridlinesVisibilitySettings[0] as Ast).chain[0].arguments
|
||||
|
|
|
@ -7,13 +7,16 @@
|
|||
import { Ast } from '@kbn/interpreter/common';
|
||||
import { ScaleType } from '@elastic/charts';
|
||||
import { State, LayerConfig } from './types';
|
||||
import { FramePublicAPI, OperationMetadata } from '../types';
|
||||
import { OperationMetadata, DatasourcePublicAPI } from '../types';
|
||||
|
||||
interface ValidLayer extends LayerConfig {
|
||||
xAccessor: NonNullable<LayerConfig['xAccessor']>;
|
||||
}
|
||||
|
||||
export const toExpression = (state: State, frame: FramePublicAPI): Ast | null => {
|
||||
export const toExpression = (
|
||||
state: State,
|
||||
datasourceLayers: Record<string, DatasourcePublicAPI>
|
||||
): Ast | null => {
|
||||
if (!state || !state.layers.length) {
|
||||
return null;
|
||||
}
|
||||
|
@ -21,19 +24,20 @@ export const toExpression = (state: State, frame: FramePublicAPI): Ast | null =>
|
|||
const metadata: Record<string, Record<string, OperationMetadata | null>> = {};
|
||||
state.layers.forEach((layer) => {
|
||||
metadata[layer.layerId] = {};
|
||||
const datasource = frame.datasourceLayers[layer.layerId];
|
||||
const datasource = datasourceLayers[layer.layerId];
|
||||
datasource.getTableSpec().forEach((column) => {
|
||||
const operation = frame.datasourceLayers[layer.layerId].getOperationForColumnId(
|
||||
column.columnId
|
||||
);
|
||||
const operation = datasourceLayers[layer.layerId].getOperationForColumnId(column.columnId);
|
||||
metadata[layer.layerId][column.columnId] = operation;
|
||||
});
|
||||
});
|
||||
|
||||
return buildExpression(state, metadata, frame);
|
||||
return buildExpression(state, metadata, datasourceLayers);
|
||||
};
|
||||
|
||||
export function toPreviewExpression(state: State, frame: FramePublicAPI) {
|
||||
export function toPreviewExpression(
|
||||
state: State,
|
||||
datasourceLayers: Record<string, DatasourcePublicAPI>
|
||||
) {
|
||||
return toExpression(
|
||||
{
|
||||
...state,
|
||||
|
@ -44,7 +48,7 @@ export function toPreviewExpression(state: State, frame: FramePublicAPI) {
|
|||
isVisible: false,
|
||||
},
|
||||
},
|
||||
frame
|
||||
datasourceLayers
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -77,7 +81,7 @@ export function getScaleType(metadata: OperationMetadata | null, defaultScale: S
|
|||
export const buildExpression = (
|
||||
state: State,
|
||||
metadata: Record<string, Record<string, OperationMetadata | null>>,
|
||||
frame?: FramePublicAPI
|
||||
datasourceLayers?: Record<string, DatasourcePublicAPI>
|
||||
): Ast | null => {
|
||||
const validLayers = state.layers.filter((layer): layer is ValidLayer =>
|
||||
Boolean(layer.xAccessor && layer.accessors.length)
|
||||
|
@ -149,8 +153,8 @@ export const buildExpression = (
|
|||
layers: validLayers.map((layer) => {
|
||||
const columnToLabel: Record<string, string> = {};
|
||||
|
||||
if (frame) {
|
||||
const datasource = frame.datasourceLayers[layer.layerId];
|
||||
if (datasourceLayers) {
|
||||
const datasource = datasourceLayers[layer.layerId];
|
||||
layer.accessors
|
||||
.concat(layer.splitAccessor ? [layer.splitAccessor] : [])
|
||||
.forEach((accessor) => {
|
||||
|
@ -162,8 +166,8 @@ export const buildExpression = (
|
|||
}
|
||||
|
||||
const xAxisOperation =
|
||||
frame &&
|
||||
frame.datasourceLayers[layer.layerId].getOperationForColumnId(layer.xAccessor);
|
||||
datasourceLayers &&
|
||||
datasourceLayers[layer.layerId].getOperationForColumnId(layer.xAccessor);
|
||||
|
||||
const isHistogramDimension = Boolean(
|
||||
xAxisOperation &&
|
||||
|
|
|
@ -339,7 +339,6 @@ export interface XYState {
|
|||
}
|
||||
|
||||
export type State = XYState;
|
||||
export type PersistableState = XYState;
|
||||
|
||||
export const visualizationTypes: VisualizationType[] = [
|
||||
{
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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<State, PersistableState> = {
|
||||
export const xyVisualization: Visualization<State> = {
|
||||
id: 'lnsXY',
|
||||
|
||||
visualizationTypes,
|
||||
|
@ -159,8 +159,6 @@ export const xyVisualization: Visualization<State, PersistableState> = {
|
|||
);
|
||||
},
|
||||
|
||||
getPersistableState: (state) => state,
|
||||
|
||||
getConfiguration(props) {
|
||||
const layer = props.state.layers.find((l) => l.layerId === props.layerId)!;
|
||||
return {
|
||||
|
|
188
x-pack/plugins/lens/server/__snapshots__/migrations.test.ts.snap
generated
Normal file
188
x-pack/plugins/lens/server/__snapshots__/migrations.test.ts.snap
generated
Normal file
|
@ -0,0 +1,188 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Lens migrations 7.10.0 references should produce a valid document 1`] = `
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"state": Object {
|
||||
"datasourceStates": Object {
|
||||
"indexpattern": Object {
|
||||
"layers": Object {
|
||||
"3b7791e9-326e-40d5-a787-b7594e48d906": Object {
|
||||
"columnOrder": Array [
|
||||
"77d8383e-f66e-471e-ae50-c427feedb5ba",
|
||||
"a5c1b82d-51de-4448-a99d-6391432c3a03",
|
||||
],
|
||||
"columns": Object {
|
||||
"77d8383e-f66e-471e-ae50-c427feedb5ba": Object {
|
||||
"dataType": "string",
|
||||
"isBucketed": true,
|
||||
"label": "Top values of geoip.country_iso_code",
|
||||
"operationType": "terms",
|
||||
"params": Object {
|
||||
"orderBy": Object {
|
||||
"columnId": "a5c1b82d-51de-4448-a99d-6391432c3a03",
|
||||
"type": "column",
|
||||
},
|
||||
"orderDirection": "desc",
|
||||
"size": 5,
|
||||
},
|
||||
"scale": "ordinal",
|
||||
"sourceField": "geoip.country_iso_code",
|
||||
},
|
||||
"a5c1b82d-51de-4448-a99d-6391432c3a03": Object {
|
||||
"dataType": "number",
|
||||
"isBucketed": false,
|
||||
"label": "Count of records",
|
||||
"operationType": "count",
|
||||
"scale": "ratio",
|
||||
"sourceField": "Records",
|
||||
},
|
||||
},
|
||||
},
|
||||
"9a27f85d-35a9-4246-81b2-48e7ee9b0707": Object {
|
||||
"columnOrder": Array [
|
||||
"96352896-c508-4fca-90d8-66e9ebfce621",
|
||||
"4ce9b4c7-2ebf-4d48-8669-0ea69d973353",
|
||||
],
|
||||
"columns": Object {
|
||||
"4ce9b4c7-2ebf-4d48-8669-0ea69d973353": Object {
|
||||
"dataType": "number",
|
||||
"isBucketed": false,
|
||||
"label": "Count of records",
|
||||
"operationType": "count",
|
||||
"scale": "ratio",
|
||||
"sourceField": "Records",
|
||||
},
|
||||
"96352896-c508-4fca-90d8-66e9ebfce621": Object {
|
||||
"dataType": "string",
|
||||
"isBucketed": true,
|
||||
"label": "Top values of geo.src",
|
||||
"operationType": "terms",
|
||||
"params": Object {
|
||||
"orderBy": Object {
|
||||
"columnId": "4ce9b4c7-2ebf-4d48-8669-0ea69d973353",
|
||||
"type": "column",
|
||||
},
|
||||
"orderDirection": "desc",
|
||||
"size": 5,
|
||||
},
|
||||
"scale": "ordinal",
|
||||
"sourceField": "geo.src",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"filters": Array [
|
||||
Object {
|
||||
"$state": Object {
|
||||
"store": "appState",
|
||||
},
|
||||
"meta": Object {
|
||||
"alias": null,
|
||||
"disabled": false,
|
||||
"index": undefined,
|
||||
"indexRefName": "filter-index-pattern-0",
|
||||
"key": "geo.src",
|
||||
"negate": true,
|
||||
"params": Object {
|
||||
"query": "CN",
|
||||
},
|
||||
"type": "phrase",
|
||||
},
|
||||
"query": Object {
|
||||
"match_phrase": Object {
|
||||
"geo.src": "CN",
|
||||
},
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"$state": Object {
|
||||
"store": "appState",
|
||||
},
|
||||
"meta": Object {
|
||||
"alias": null,
|
||||
"disabled": false,
|
||||
"index": undefined,
|
||||
"indexRefName": "filter-index-pattern-1",
|
||||
"key": "geoip.country_iso_code",
|
||||
"negate": true,
|
||||
"params": Object {
|
||||
"query": "US",
|
||||
},
|
||||
"type": "phrase",
|
||||
},
|
||||
"query": Object {
|
||||
"match_phrase": Object {
|
||||
"geoip.country_iso_code": "US",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
"query": Object {
|
||||
"language": "kuery",
|
||||
"query": "NOT bytes > 5000",
|
||||
},
|
||||
"visualization": Object {
|
||||
"fittingFunction": "None",
|
||||
"layers": Array [
|
||||
Object {
|
||||
"accessors": Array [
|
||||
"4ce9b4c7-2ebf-4d48-8669-0ea69d973353",
|
||||
],
|
||||
"layerId": "9a27f85d-35a9-4246-81b2-48e7ee9b0707",
|
||||
"position": "top",
|
||||
"seriesType": "bar",
|
||||
"showGridlines": false,
|
||||
"xAccessor": "96352896-c508-4fca-90d8-66e9ebfce621",
|
||||
},
|
||||
Object {
|
||||
"accessors": Array [
|
||||
"a5c1b82d-51de-4448-a99d-6391432c3a03",
|
||||
],
|
||||
"layerId": "3b7791e9-326e-40d5-a787-b7594e48d906",
|
||||
"seriesType": "bar",
|
||||
"xAccessor": "77d8383e-f66e-471e-ae50-c427feedb5ba",
|
||||
},
|
||||
],
|
||||
"legend": Object {
|
||||
"isVisible": true,
|
||||
"position": "right",
|
||||
},
|
||||
"preferredSeriesType": "bar",
|
||||
},
|
||||
},
|
||||
"title": "mylens",
|
||||
"visualizationType": "lnsXY",
|
||||
},
|
||||
"references": Array [
|
||||
Object {
|
||||
"id": "ff959d40-b880-11e8-a6d9-e546fe2bba5f",
|
||||
"name": "indexpattern-datasource-current-indexpattern",
|
||||
"type": "index-pattern",
|
||||
},
|
||||
Object {
|
||||
"id": "ff959d40-b880-11e8-a6d9-e546fe2bba5f",
|
||||
"name": "indexpattern-datasource-layer-3b7791e9-326e-40d5-a787-b7594e48d906",
|
||||
"type": "index-pattern",
|
||||
},
|
||||
Object {
|
||||
"id": "90943e30-9a47-11e8-b64d-95841ca0b247",
|
||||
"name": "indexpattern-datasource-layer-9a27f85d-35a9-4246-81b2-48e7ee9b0707",
|
||||
"type": "index-pattern",
|
||||
},
|
||||
Object {
|
||||
"id": "90943e30-9a47-11e8-b64d-95841ca0b247",
|
||||
"name": "filter-index-pattern-0",
|
||||
"type": "index-pattern",
|
||||
},
|
||||
Object {
|
||||
"id": "ff959d40-b880-11e8-a6d9-e546fe2bba5f",
|
||||
"name": "filter-index-pattern-1",
|
||||
"type": "index-pattern",
|
||||
},
|
||||
],
|
||||
"type": "lens",
|
||||
}
|
||||
`;
|
|
@ -278,4 +278,233 @@ describe('Lens migrations', () => {
|
|||
expect(result).toEqual(input);
|
||||
});
|
||||
});
|
||||
|
||||
describe('7.10.0 references', () => {
|
||||
const context = {} as SavedObjectMigrationContext;
|
||||
|
||||
const example = {
|
||||
attributes: {
|
||||
description: '',
|
||||
expression:
|
||||
'kibana\n| kibana_context query="{\\"query\\":\\"NOT bytes > 5000\\",\\"language\\":\\"kuery\\"}" \n filters="[{\\"meta\\":{\\"index\\":\\"90943e30-9a47-11e8-b64d-95841ca0b247\\",\\"alias\\":null,\\"negate\\":true,\\"disabled\\":false,\\"type\\":\\"phrase\\",\\"key\\":\\"geo.src\\",\\"params\\":{\\"query\\":\\"CN\\"}},\\"query\\":{\\"match_phrase\\":{\\"geo.src\\":\\"CN\\"}},\\"$state\\":{\\"store\\":\\"appState\\"}},{\\"meta\\":{\\"index\\":\\"ff959d40-b880-11e8-a6d9-e546fe2bba5f\\",\\"alias\\":null,\\"negate\\":true,\\"disabled\\":false,\\"type\\":\\"phrase\\",\\"key\\":\\"geoip.country_iso_code\\",\\"params\\":{\\"query\\":\\"US\\"}},\\"query\\":{\\"match_phrase\\":{\\"geoip.country_iso_code\\":\\"US\\"}},\\"$state\\":{\\"store\\":\\"appState\\"}}]"\n| lens_merge_tables layerIds="9a27f85d-35a9-4246-81b2-48e7ee9b0707"\n layerIds="3b7791e9-326e-40d5-a787-b7594e48d906" \n tables={esaggs index="90943e30-9a47-11e8-b64d-95841ca0b247" metricsAtAllLevels=true partialRows=true includeFormatHints=true aggConfigs="[{\\"id\\":\\"96352896-c508-4fca-90d8-66e9ebfce621\\",\\"enabled\\":true,\\"type\\":\\"terms\\",\\"schema\\":\\"segment\\",\\"params\\":{\\"field\\":\\"geo.src\\",\\"orderBy\\":\\"4ce9b4c7-2ebf-4d48-8669-0ea69d973353\\",\\"order\\":\\"desc\\",\\"size\\":5,\\"otherBucket\\":false,\\"otherBucketLabel\\":\\"Other\\",\\"missingBucket\\":false,\\"missingBucketLabel\\":\\"Missing\\"}},{\\"id\\":\\"4ce9b4c7-2ebf-4d48-8669-0ea69d973353\\",\\"enabled\\":true,\\"type\\":\\"count\\",\\"schema\\":\\"metric\\",\\"params\\":{}}]" | lens_rename_columns idMap="{\\"col-0-96352896-c508-4fca-90d8-66e9ebfce621\\":{\\"label\\":\\"Top values of geo.src\\",\\"dataType\\":\\"string\\",\\"operationType\\":\\"terms\\",\\"scale\\":\\"ordinal\\",\\"sourceField\\":\\"geo.src\\",\\"isBucketed\\":true,\\"params\\":{\\"size\\":5,\\"orderBy\\":{\\"type\\":\\"column\\",\\"columnId\\":\\"4ce9b4c7-2ebf-4d48-8669-0ea69d973353\\"},\\"orderDirection\\":\\"desc\\"},\\"id\\":\\"96352896-c508-4fca-90d8-66e9ebfce621\\"},\\"col-1-4ce9b4c7-2ebf-4d48-8669-0ea69d973353\\":{\\"label\\":\\"Count of records\\",\\"dataType\\":\\"number\\",\\"operationType\\":\\"count\\",\\"isBucketed\\":false,\\"scale\\":\\"ratio\\",\\"sourceField\\":\\"Records\\",\\"id\\":\\"4ce9b4c7-2ebf-4d48-8669-0ea69d973353\\"}}"}\n tables={esaggs index="ff959d40-b880-11e8-a6d9-e546fe2bba5f" metricsAtAllLevels=true partialRows=true includeFormatHints=true aggConfigs="[{\\"id\\":\\"77d8383e-f66e-471e-ae50-c427feedb5ba\\",\\"enabled\\":true,\\"type\\":\\"terms\\",\\"schema\\":\\"segment\\",\\"params\\":{\\"field\\":\\"geoip.country_iso_code\\",\\"orderBy\\":\\"a5c1b82d-51de-4448-a99d-6391432c3a03\\",\\"order\\":\\"desc\\",\\"size\\":5,\\"otherBucket\\":false,\\"otherBucketLabel\\":\\"Other\\",\\"missingBucket\\":false,\\"missingBucketLabel\\":\\"Missing\\"}},{\\"id\\":\\"a5c1b82d-51de-4448-a99d-6391432c3a03\\",\\"enabled\\":true,\\"type\\":\\"count\\",\\"schema\\":\\"metric\\",\\"params\\":{}}]" | lens_rename_columns idMap="{\\"col-0-77d8383e-f66e-471e-ae50-c427feedb5ba\\":{\\"label\\":\\"Top values of geoip.country_iso_code\\",\\"dataType\\":\\"string\\",\\"operationType\\":\\"terms\\",\\"scale\\":\\"ordinal\\",\\"sourceField\\":\\"geoip.country_iso_code\\",\\"isBucketed\\":true,\\"params\\":{\\"size\\":5,\\"orderBy\\":{\\"type\\":\\"column\\",\\"columnId\\":\\"a5c1b82d-51de-4448-a99d-6391432c3a03\\"},\\"orderDirection\\":\\"desc\\"},\\"id\\":\\"77d8383e-f66e-471e-ae50-c427feedb5ba\\"},\\"col-1-a5c1b82d-51de-4448-a99d-6391432c3a03\\":{\\"label\\":\\"Count of records\\",\\"dataType\\":\\"number\\",\\"operationType\\":\\"count\\",\\"isBucketed\\":false,\\"scale\\":\\"ratio\\",\\"sourceField\\":\\"Records\\",\\"id\\":\\"a5c1b82d-51de-4448-a99d-6391432c3a03\\"}}"}\n| lens_xy_chart xTitle="Top values of geo.src" yTitle="Count of records" legend={lens_xy_legendConfig isVisible=true position="right"} fittingFunction="None" \n layers={lens_xy_layer layerId="9a27f85d-35a9-4246-81b2-48e7ee9b0707" hide=false xAccessor="96352896-c508-4fca-90d8-66e9ebfce621" yScaleType="linear" xScaleType="ordinal" isHistogram=false seriesType="bar" accessors="4ce9b4c7-2ebf-4d48-8669-0ea69d973353" columnToLabel="{\\"4ce9b4c7-2ebf-4d48-8669-0ea69d973353\\":\\"Count of records\\"}"}\n layers={lens_xy_layer layerId="3b7791e9-326e-40d5-a787-b7594e48d906" hide=false xAccessor="77d8383e-f66e-471e-ae50-c427feedb5ba" yScaleType="linear" xScaleType="ordinal" isHistogram=false seriesType="bar" accessors="a5c1b82d-51de-4448-a99d-6391432c3a03" columnToLabel="{\\"a5c1b82d-51de-4448-a99d-6391432c3a03\\":\\"Count of records [1]\\"}"}',
|
||||
state: {
|
||||
datasourceMetaData: {
|
||||
filterableIndexPatterns: [
|
||||
{ id: '90943e30-9a47-11e8-b64d-95841ca0b247', title: 'kibana_sample_data_logs' },
|
||||
{ id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', title: 'kibana_sample_data_ecommerce' },
|
||||
],
|
||||
},
|
||||
datasourceStates: {
|
||||
indexpattern: {
|
||||
currentIndexPatternId: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
|
||||
layers: {
|
||||
'3b7791e9-326e-40d5-a787-b7594e48d906': {
|
||||
columnOrder: [
|
||||
'77d8383e-f66e-471e-ae50-c427feedb5ba',
|
||||
'a5c1b82d-51de-4448-a99d-6391432c3a03',
|
||||
],
|
||||
columns: {
|
||||
'77d8383e-f66e-471e-ae50-c427feedb5ba': {
|
||||
dataType: 'string',
|
||||
isBucketed: true,
|
||||
label: 'Top values of geoip.country_iso_code',
|
||||
operationType: 'terms',
|
||||
params: {
|
||||
orderBy: {
|
||||
columnId: 'a5c1b82d-51de-4448-a99d-6391432c3a03',
|
||||
type: 'column',
|
||||
},
|
||||
orderDirection: 'desc',
|
||||
size: 5,
|
||||
},
|
||||
scale: 'ordinal',
|
||||
sourceField: 'geoip.country_iso_code',
|
||||
},
|
||||
'a5c1b82d-51de-4448-a99d-6391432c3a03': {
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
label: 'Count of records',
|
||||
operationType: 'count',
|
||||
scale: 'ratio',
|
||||
sourceField: 'Records',
|
||||
},
|
||||
},
|
||||
indexPatternId: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
|
||||
},
|
||||
'9a27f85d-35a9-4246-81b2-48e7ee9b0707': {
|
||||
columnOrder: [
|
||||
'96352896-c508-4fca-90d8-66e9ebfce621',
|
||||
'4ce9b4c7-2ebf-4d48-8669-0ea69d973353',
|
||||
],
|
||||
columns: {
|
||||
'4ce9b4c7-2ebf-4d48-8669-0ea69d973353': {
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
label: 'Count of records',
|
||||
operationType: 'count',
|
||||
scale: 'ratio',
|
||||
sourceField: 'Records',
|
||||
},
|
||||
'96352896-c508-4fca-90d8-66e9ebfce621': {
|
||||
dataType: 'string',
|
||||
isBucketed: true,
|
||||
label: 'Top values of geo.src',
|
||||
operationType: 'terms',
|
||||
params: {
|
||||
orderBy: {
|
||||
columnId: '4ce9b4c7-2ebf-4d48-8669-0ea69d973353',
|
||||
type: 'column',
|
||||
},
|
||||
orderDirection: 'desc',
|
||||
size: 5,
|
||||
},
|
||||
scale: 'ordinal',
|
||||
sourceField: 'geo.src',
|
||||
},
|
||||
},
|
||||
indexPatternId: '90943e30-9a47-11e8-b64d-95841ca0b247',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
filters: [
|
||||
{
|
||||
$state: { store: 'appState' },
|
||||
meta: {
|
||||
alias: null,
|
||||
disabled: false,
|
||||
index: '90943e30-9a47-11e8-b64d-95841ca0b247',
|
||||
key: 'geo.src',
|
||||
negate: true,
|
||||
params: { query: 'CN' },
|
||||
type: 'phrase',
|
||||
},
|
||||
query: { match_phrase: { 'geo.src': 'CN' } },
|
||||
},
|
||||
{
|
||||
$state: { store: 'appState' },
|
||||
meta: {
|
||||
alias: null,
|
||||
disabled: false,
|
||||
index: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
|
||||
key: 'geoip.country_iso_code',
|
||||
negate: true,
|
||||
params: { query: 'US' },
|
||||
type: 'phrase',
|
||||
},
|
||||
query: { match_phrase: { 'geoip.country_iso_code': 'US' } },
|
||||
},
|
||||
],
|
||||
query: { language: 'kuery', query: 'NOT bytes > 5000' },
|
||||
visualization: {
|
||||
fittingFunction: 'None',
|
||||
layers: [
|
||||
{
|
||||
accessors: ['4ce9b4c7-2ebf-4d48-8669-0ea69d973353'],
|
||||
layerId: '9a27f85d-35a9-4246-81b2-48e7ee9b0707',
|
||||
position: 'top',
|
||||
seriesType: 'bar',
|
||||
showGridlines: false,
|
||||
xAccessor: '96352896-c508-4fca-90d8-66e9ebfce621',
|
||||
},
|
||||
{
|
||||
accessors: ['a5c1b82d-51de-4448-a99d-6391432c3a03'],
|
||||
layerId: '3b7791e9-326e-40d5-a787-b7594e48d906',
|
||||
seriesType: 'bar',
|
||||
xAccessor: '77d8383e-f66e-471e-ae50-c427feedb5ba',
|
||||
},
|
||||
],
|
||||
legend: { isVisible: true, position: 'right' },
|
||||
preferredSeriesType: 'bar',
|
||||
},
|
||||
},
|
||||
title: 'mylens',
|
||||
visualizationType: 'lnsXY',
|
||||
},
|
||||
type: 'lens',
|
||||
};
|
||||
|
||||
it('should remove expression', () => {
|
||||
const result = migrations['7.10.0'](example, context);
|
||||
expect(result.attributes.expression).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should list references for layers', () => {
|
||||
const result = migrations['7.10.0'](example, context);
|
||||
expect(
|
||||
result.references?.find(
|
||||
(ref) => ref.name === 'indexpattern-datasource-layer-3b7791e9-326e-40d5-a787-b7594e48d906'
|
||||
)?.id
|
||||
).toEqual('ff959d40-b880-11e8-a6d9-e546fe2bba5f');
|
||||
expect(
|
||||
result.references?.find(
|
||||
(ref) => ref.name === 'indexpattern-datasource-layer-9a27f85d-35a9-4246-81b2-48e7ee9b0707'
|
||||
)?.id
|
||||
).toEqual('90943e30-9a47-11e8-b64d-95841ca0b247');
|
||||
});
|
||||
|
||||
it('should remove index pattern ids from layers', () => {
|
||||
const result = migrations['7.10.0'](example, context);
|
||||
expect(
|
||||
result.attributes.state.datasourceStates.indexpattern.layers[
|
||||
'3b7791e9-326e-40d5-a787-b7594e48d906'
|
||||
].indexPatternId
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
result.attributes.state.datasourceStates.indexpattern.layers[
|
||||
'9a27f85d-35a9-4246-81b2-48e7ee9b0707'
|
||||
].indexPatternId
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should remove datsource meta data', () => {
|
||||
const result = migrations['7.10.0'](example, context);
|
||||
expect(result.attributes.state.datasourceMetaData).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should list references for filters', () => {
|
||||
const result = migrations['7.10.0'](example, context);
|
||||
expect(result.references?.find((ref) => ref.name === 'filter-index-pattern-0')?.id).toEqual(
|
||||
'90943e30-9a47-11e8-b64d-95841ca0b247'
|
||||
);
|
||||
expect(result.references?.find((ref) => ref.name === 'filter-index-pattern-1')?.id).toEqual(
|
||||
'ff959d40-b880-11e8-a6d9-e546fe2bba5f'
|
||||
);
|
||||
});
|
||||
|
||||
it('should remove index pattern ids from filters', () => {
|
||||
const result = migrations['7.10.0'](example, context);
|
||||
expect(result.attributes.state.filters[0].meta.index).toBeUndefined();
|
||||
expect(result.attributes.state.filters[0].meta.indexRefName).toEqual(
|
||||
'filter-index-pattern-0'
|
||||
);
|
||||
expect(result.attributes.state.filters[1].meta.index).toBeUndefined();
|
||||
expect(result.attributes.state.filters[1].meta.indexRefName).toEqual(
|
||||
'filter-index-pattern-1'
|
||||
);
|
||||
});
|
||||
|
||||
it('should list reference for current index pattern', () => {
|
||||
const result = migrations['7.10.0'](example, context);
|
||||
expect(
|
||||
result.references?.find(
|
||||
(ref) => ref.name === 'indexpattern-datasource-current-indexpattern'
|
||||
)?.id
|
||||
).toEqual('ff959d40-b880-11e8-a6d9-e546fe2bba5f');
|
||||
});
|
||||
|
||||
it('should remove current index pattern id from datasource state', () => {
|
||||
const result = migrations['7.10.0'](example, context);
|
||||
expect(
|
||||
result.attributes.state.datasourceStates.indexpattern.currentIndexPatternId
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should produce a valid document', () => {
|
||||
const result = migrations['7.10.0'](example, context);
|
||||
// changes to the outcome of this are critical - this test is a safe guard to not introduce changes accidentally
|
||||
// if this test fails, make extra sure it's expected
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,11 +6,16 @@
|
|||
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { fromExpression, toExpression, Ast, ExpressionFunctionAST } from '@kbn/interpreter/common';
|
||||
import { SavedObjectMigrationMap, SavedObjectMigrationFn } from 'src/core/server';
|
||||
import {
|
||||
SavedObjectMigrationMap,
|
||||
SavedObjectMigrationFn,
|
||||
SavedObjectReference,
|
||||
SavedObjectUnsanitizedDoc,
|
||||
} from 'src/core/server';
|
||||
import { Query, Filter } from 'src/plugins/data/public';
|
||||
import { PersistableFilter } from '../common';
|
||||
|
||||
interface LensDocShape<VisualizationState = unknown> {
|
||||
id?: string;
|
||||
type?: string;
|
||||
interface LensDocShapePre710<VisualizationState = unknown> {
|
||||
visualizationType: string | null;
|
||||
title: string;
|
||||
expression: string | null;
|
||||
|
@ -18,6 +23,32 @@ interface LensDocShape<VisualizationState = unknown> {
|
|||
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<string, unknown>;
|
||||
indexPatternId: string;
|
||||
}
|
||||
>;
|
||||
};
|
||||
};
|
||||
visualization: VisualizationState;
|
||||
query: Query;
|
||||
filters: Filter[];
|
||||
};
|
||||
}
|
||||
|
||||
interface LensDocShape<VisualizationState = unknown> {
|
||||
id?: string;
|
||||
type?: string;
|
||||
visualizationType: string | null;
|
||||
title: string;
|
||||
state: {
|
||||
datasourceStates: {
|
||||
// This is hardcoded as our only datasource
|
||||
indexpattern: {
|
||||
|
@ -31,8 +62,8 @@ interface LensDocShape<VisualizationState = unknown> {
|
|||
};
|
||||
};
|
||||
visualization: VisualizationState;
|
||||
query: unknown;
|
||||
filters: unknown[];
|
||||
query: Query;
|
||||
filters: PersistableFilter[];
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -55,7 +86,10 @@ interface XYStatePost77 {
|
|||
* Removes the `lens_auto_date` subexpression from a stored expression
|
||||
* string. For example: aggConfigs={lens_auto_date aggConfigs="JSON string"}
|
||||
*/
|
||||
const removeLensAutoDate: SavedObjectMigrationFn<LensDocShape, LensDocShape> = (doc, context) => {
|
||||
const removeLensAutoDate: SavedObjectMigrationFn<LensDocShapePre710, LensDocShapePre710> = (
|
||||
doc,
|
||||
context
|
||||
) => {
|
||||
const expression = doc.attributes.expression;
|
||||
if (!expression) {
|
||||
return doc;
|
||||
|
@ -112,7 +146,10 @@ const removeLensAutoDate: SavedObjectMigrationFn<LensDocShape, LensDocShape> = (
|
|||
/**
|
||||
* Adds missing timeField arguments to esaggs in the Lens expression
|
||||
*/
|
||||
const addTimeFieldToEsaggs: SavedObjectMigrationFn<LensDocShape, LensDocShape> = (doc, context) => {
|
||||
const addTimeFieldToEsaggs: SavedObjectMigrationFn<LensDocShapePre710, LensDocShapePre710> = (
|
||||
doc,
|
||||
context
|
||||
) => {
|
||||
const expression = doc.attributes.expression;
|
||||
if (!expression) {
|
||||
return doc;
|
||||
|
@ -174,14 +211,14 @@ const addTimeFieldToEsaggs: SavedObjectMigrationFn<LensDocShape, LensDocShape> =
|
|||
};
|
||||
|
||||
const removeInvalidAccessors: SavedObjectMigrationFn<
|
||||
LensDocShape<XYStatePre77>,
|
||||
LensDocShape<XYStatePost77>
|
||||
LensDocShapePre710<XYStatePre77>,
|
||||
LensDocShapePre710<XYStatePost77>
|
||||
> = (doc) => {
|
||||
const newDoc = cloneDeep(doc);
|
||||
if (newDoc.attributes.visualizationType === 'lnsXY') {
|
||||
const datasourceLayers = newDoc.attributes.state.datasourceStates.indexpattern.layers || {};
|
||||
const xyState = newDoc.attributes.state.visualization;
|
||||
(newDoc.attributes as LensDocShape<
|
||||
(newDoc.attributes as LensDocShapePre710<
|
||||
XYStatePost77
|
||||
>).state.visualization.layers = xyState.layers.map((layer: XYLayerPre77) => {
|
||||
const layerId = layer.layerId;
|
||||
|
@ -197,9 +234,86 @@ const removeInvalidAccessors: SavedObjectMigrationFn<
|
|||
return newDoc;
|
||||
};
|
||||
|
||||
const extractReferences: SavedObjectMigrationFn<LensDocShapePre710, LensDocShape> = ({
|
||||
attributes,
|
||||
references,
|
||||
...docMeta
|
||||
}) => {
|
||||
const savedObjectReferences: SavedObjectReference[] = [];
|
||||
// add currently selected index pattern to reference list
|
||||
savedObjectReferences.push({
|
||||
type: 'index-pattern',
|
||||
id: attributes.state.datasourceStates.indexpattern.currentIndexPatternId,
|
||||
name: 'indexpattern-datasource-current-indexpattern',
|
||||
});
|
||||
|
||||
// add layer index patterns to list and remove index pattern ids from layers
|
||||
const persistableLayers: Record<
|
||||
string,
|
||||
Omit<
|
||||
LensDocShapePre710['state']['datasourceStates']['indexpattern']['layers'][string],
|
||||
'indexPatternId'
|
||||
>
|
||||
> = {};
|
||||
Object.entries(attributes.state.datasourceStates.indexpattern.layers).forEach(
|
||||
([layerId, { indexPatternId, ...persistableLayer }]) => {
|
||||
savedObjectReferences.push({
|
||||
type: 'index-pattern',
|
||||
id: indexPatternId,
|
||||
name: `indexpattern-datasource-layer-${layerId}`,
|
||||
});
|
||||
persistableLayers[layerId] = persistableLayer;
|
||||
}
|
||||
);
|
||||
|
||||
// add filter index patterns to reference list and remove index pattern ids from filter definitions
|
||||
const persistableFilters = attributes.state.filters.map((filterRow, i) => {
|
||||
if (!filterRow.meta || !filterRow.meta.index) {
|
||||
return filterRow;
|
||||
}
|
||||
const refName = `filter-index-pattern-${i}`;
|
||||
savedObjectReferences.push({
|
||||
name: refName,
|
||||
type: 'index-pattern',
|
||||
id: filterRow.meta.index,
|
||||
});
|
||||
return {
|
||||
...filterRow,
|
||||
meta: {
|
||||
...filterRow.meta,
|
||||
indexRefName: refName,
|
||||
index: undefined,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// put together new saved object format
|
||||
const newDoc: SavedObjectUnsanitizedDoc<LensDocShape> = {
|
||||
...docMeta,
|
||||
references: savedObjectReferences,
|
||||
attributes: {
|
||||
visualizationType: attributes.visualizationType,
|
||||
title: attributes.title,
|
||||
state: {
|
||||
datasourceStates: {
|
||||
indexpattern: {
|
||||
layers: persistableLayers,
|
||||
},
|
||||
},
|
||||
visualization: attributes.state.visualization,
|
||||
query: attributes.state.query,
|
||||
filters: persistableFilters,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return newDoc;
|
||||
};
|
||||
|
||||
export const migrations: SavedObjectMigrationMap = {
|
||||
'7.7.0': removeInvalidAccessors,
|
||||
// The order of these migrations matter, since the timefield migration relies on the aggConfigs
|
||||
// sitting directly on the esaggs as an argument and not a nested function (which lens_auto_date was).
|
||||
'7.8.0': (doc, context) => addTimeFieldToEsaggs(removeLensAutoDate(doc, context), context),
|
||||
'7.10.0': extractReferences,
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue