mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[Lens] give data plugin control of filter extraction, injection, and migrations (#120305)
This commit is contained in:
parent
1aadcd34c6
commit
efc07eed86
42 changed files with 1092 additions and 378 deletions
|
@ -20,6 +20,7 @@ import { UsageCollectionSetup } from '../../usage_collection/server';
|
|||
import { AutocompleteService } from './autocomplete';
|
||||
import { FieldFormatsSetup, FieldFormatsStart } from '../../field_formats/server';
|
||||
import { getUiSettings } from './ui_settings';
|
||||
import { QuerySetup } from './query';
|
||||
|
||||
interface DataEnhancements {
|
||||
search: SearchEnhancements;
|
||||
|
@ -27,6 +28,7 @@ interface DataEnhancements {
|
|||
|
||||
export interface DataPluginSetup {
|
||||
search: ISearchSetup;
|
||||
query: QuerySetup;
|
||||
/**
|
||||
* @deprecated - use "fieldFormats" plugin directly instead
|
||||
*/
|
||||
|
@ -88,7 +90,7 @@ export class DataServerPlugin
|
|||
{ bfetch, expressions, usageCollection, fieldFormats }: DataPluginSetupDependencies
|
||||
) {
|
||||
this.scriptsService.setup(core);
|
||||
this.queryService.setup(core);
|
||||
const querySetup = this.queryService.setup(core);
|
||||
this.autocompleteService.setup(core);
|
||||
this.kqlTelemetryService.setup(core, { usageCollection });
|
||||
|
||||
|
@ -105,6 +107,7 @@ export class DataServerPlugin
|
|||
searchSetup.__enhance(enhancements.search);
|
||||
},
|
||||
search: searchSetup,
|
||||
query: querySetup,
|
||||
fieldFormats,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -7,3 +7,4 @@
|
|||
*/
|
||||
|
||||
export { QueryService } from './query_service';
|
||||
export type { QuerySetup } from './query_service';
|
||||
|
|
|
@ -36,3 +36,6 @@ export class QueryService implements Plugin<void> {
|
|||
|
||||
public start() {}
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export type QuerySetup = ReturnType<QueryService['setup']>;
|
||||
|
|
|
@ -92,8 +92,8 @@ export interface PersistableState<P extends SerializableRecord = SerializableRec
|
|||
*/
|
||||
export type MigrateFunctionsObject = { [semver: string]: MigrateFunction<any, any> };
|
||||
export type MigrateFunction<
|
||||
FromVersion extends SerializableRecord = SerializableRecord,
|
||||
ToVersion extends SerializableRecord = SerializableRecord
|
||||
FromVersion extends Serializable = SerializableRecord,
|
||||
ToVersion extends Serializable = SerializableRecord
|
||||
> = (state: FromVersion) => ToVersion;
|
||||
|
||||
/**
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import { SavedObject, SavedObjectsFindResponse } from 'kibana/server';
|
||||
import { lensEmbeddableFactory } from '../../../lens/server/embeddable/lens_embeddable_factory';
|
||||
import { makeLensEmbeddableFactory } from '../../../lens/server/embeddable/make_lens_embeddable_factory';
|
||||
import { SECURITY_SOLUTION_OWNER } from '../../common/constants';
|
||||
import {
|
||||
AssociationType,
|
||||
|
@ -879,7 +879,7 @@ describe('common utils', () => {
|
|||
].join('\n\n');
|
||||
|
||||
const extractedReferences = extractLensReferencesFromCommentString(
|
||||
lensEmbeddableFactory,
|
||||
makeLensEmbeddableFactory({}),
|
||||
commentString
|
||||
);
|
||||
|
||||
|
@ -977,12 +977,16 @@ describe('common utils', () => {
|
|||
)}},"editMode":false}}`,
|
||||
].join('\n\n');
|
||||
|
||||
const updatedReferences = getOrUpdateLensReferences(lensEmbeddableFactory, newCommentString, {
|
||||
references: currentCommentReferences,
|
||||
attributes: {
|
||||
comment: currentCommentString,
|
||||
},
|
||||
} as SavedObject<CommentRequestUserType>);
|
||||
const updatedReferences = getOrUpdateLensReferences(
|
||||
makeLensEmbeddableFactory({}),
|
||||
newCommentString,
|
||||
{
|
||||
references: currentCommentReferences,
|
||||
attributes: {
|
||||
comment: currentCommentString,
|
||||
},
|
||||
} as SavedObject<CommentRequestUserType>
|
||||
);
|
||||
|
||||
const expectedReferences = [
|
||||
...nonLensCurrentCommentReferences,
|
||||
|
|
|
@ -17,7 +17,7 @@ import {
|
|||
} from '../../../common/utils/markdown_plugins/utils';
|
||||
|
||||
import { savedObjectsServiceMock } from '../../../../../../src/core/server/mocks';
|
||||
import { lensEmbeddableFactory } from '../../../../lens/server/embeddable/lens_embeddable_factory';
|
||||
import { makeLensEmbeddableFactory } from '../../../../lens/server/embeddable/make_lens_embeddable_factory';
|
||||
import { LensDocShape715 } from '../../../../lens/server';
|
||||
import {
|
||||
SavedObjectReference,
|
||||
|
@ -32,7 +32,7 @@ import { SerializableRecord } from '@kbn/utility-types';
|
|||
|
||||
describe('comments migrations', () => {
|
||||
const migrations = createCommentsMigrations({
|
||||
lensEmbeddableFactory,
|
||||
lensEmbeddableFactory: makeLensEmbeddableFactory({}),
|
||||
});
|
||||
|
||||
const contextMock = savedObjectsServiceMock.createMigrationContext();
|
||||
|
|
|
@ -126,11 +126,13 @@ describe('Lens App', () => {
|
|||
defaultSavedObjectId = '1234';
|
||||
defaultDoc = {
|
||||
savedObjectId: defaultSavedObjectId,
|
||||
visualizationType: 'testVis',
|
||||
type: 'lens',
|
||||
title: 'An extremely cool default document!',
|
||||
expression: 'definitely a valid expression',
|
||||
state: {
|
||||
query: 'lucene',
|
||||
filters: [{ query: { match_phrase: { src: 'test' } } }],
|
||||
filters: [{ query: { match_phrase: { src: 'test' } }, meta: { index: 'index-pattern-0' } }],
|
||||
},
|
||||
references: [{ type: 'index-pattern', id: '1', name: 'index-pattern-0' }],
|
||||
} as unknown as Document;
|
||||
|
@ -685,7 +687,7 @@ describe('Lens App', () => {
|
|||
savedObjectId: defaultSavedObjectId,
|
||||
title: 'hello there2',
|
||||
state: expect.objectContaining({
|
||||
filters: [unpinned],
|
||||
filters: services.data.query.filterManager.inject([unpinned], []),
|
||||
}),
|
||||
}),
|
||||
true,
|
||||
|
@ -1252,24 +1254,28 @@ describe('Lens App', () => {
|
|||
});
|
||||
|
||||
it('should not confirm when changes are saved', async () => {
|
||||
const { props } = await mountWith({
|
||||
preloadedState: {
|
||||
persistedDoc: {
|
||||
...defaultDoc,
|
||||
state: {
|
||||
...defaultDoc.state,
|
||||
datasourceStates: { testDatasource: {} },
|
||||
visualization: {},
|
||||
},
|
||||
},
|
||||
isSaveable: true,
|
||||
...(defaultDoc.state as Partial<LensAppState>),
|
||||
visualization: {
|
||||
activeId: 'testVis',
|
||||
state: {},
|
||||
const preloadedState = {
|
||||
persistedDoc: {
|
||||
...defaultDoc,
|
||||
state: {
|
||||
...defaultDoc.state,
|
||||
datasourceStates: { testDatasource: {} },
|
||||
visualization: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
isSaveable: true,
|
||||
...(defaultDoc.state as Partial<LensAppState>),
|
||||
visualization: {
|
||||
activeId: 'testVis',
|
||||
state: {},
|
||||
},
|
||||
};
|
||||
|
||||
const customProps = makeDefaultProps();
|
||||
customProps.datasourceMap.testDatasource.isEqual = () => true; // if this returns false, the documents won't be accounted equal
|
||||
|
||||
const { props } = await mountWith({ preloadedState, props: customProps });
|
||||
|
||||
const lastCall = props.onAppLeave.mock.calls[props.onAppLeave.mock.calls.length - 1][0];
|
||||
lastCall({ default: defaultLeave, confirm: confirmLeave });
|
||||
expect(defaultLeave).toHaveBeenCalled();
|
||||
|
|
|
@ -7,8 +7,7 @@
|
|||
|
||||
import './app.scss';
|
||||
|
||||
import { isEqual } from 'lodash';
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiBreadcrumb } from '@elastic/eui';
|
||||
import {
|
||||
|
@ -32,13 +31,10 @@ import {
|
|||
DispatchSetState,
|
||||
selectSavedObjectFormat,
|
||||
} from '../state_management';
|
||||
import {
|
||||
SaveModalContainer,
|
||||
getLastKnownDocWithoutPinnedFilters,
|
||||
runSaveLensVisualization,
|
||||
} from './save_modal_container';
|
||||
import { SaveModalContainer, runSaveLensVisualization } from './save_modal_container';
|
||||
import { LensInspector } from '../lens_inspector_service';
|
||||
import { getEditPath } from '../../common';
|
||||
import { isLensEqual } from './lens_document_equality';
|
||||
|
||||
export type SaveProps = Omit<OnSaveProps, 'onTitleDuplicate' | 'newDescription'> & {
|
||||
returnToOrigin: boolean;
|
||||
|
@ -92,8 +88,17 @@ export function App({
|
|||
isSaveable,
|
||||
} = useLensSelector((state) => state.lens);
|
||||
|
||||
const selectorDependencies = useMemo(
|
||||
() => ({
|
||||
datasourceMap,
|
||||
visualizationMap,
|
||||
extractFilterReferences: data.query.filterManager.extract,
|
||||
}),
|
||||
[datasourceMap, visualizationMap, data.query.filterManager.extract]
|
||||
);
|
||||
|
||||
const currentDoc = useLensSelector((state) =>
|
||||
selectSavedObjectFormat(state, datasourceMap, visualizationMap)
|
||||
selectSavedObjectFormat(state, selectorDependencies)
|
||||
);
|
||||
|
||||
// Used to show a popover that guides the user towards changing the date range when no data is available.
|
||||
|
@ -146,12 +151,9 @@ export function App({
|
|||
|
||||
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 (
|
||||
application.capabilities.visualize.save &&
|
||||
!isEqual(persistedDoc?.state, getLastKnownDocWithoutPinnedFilters(lastKnownDoc)?.state) &&
|
||||
!isLensEqual(persistedDoc, lastKnownDoc, data.query.filterManager.inject, datasourceMap) &&
|
||||
(isSaveable || persistedDoc)
|
||||
) {
|
||||
return actions.confirm(
|
||||
|
@ -166,7 +168,15 @@ export function App({
|
|||
return actions.default();
|
||||
}
|
||||
});
|
||||
}, [onAppLeave, lastKnownDoc, isSaveable, persistedDoc, application.capabilities.visualize.save]);
|
||||
}, [
|
||||
onAppLeave,
|
||||
lastKnownDoc,
|
||||
isSaveable,
|
||||
persistedDoc,
|
||||
application.capabilities.visualize.save,
|
||||
data.query.filterManager.inject,
|
||||
datasourceMap,
|
||||
]);
|
||||
|
||||
const getLegacyUrlConflictCallout = useCallback(() => {
|
||||
// This function returns a callout component *if* we have encountered a "legacy URL conflict" scenario
|
||||
|
|
|
@ -0,0 +1,248 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { Filter, FilterStateStore } from '@kbn/es-query';
|
||||
import { isLensEqual } from './lens_document_equality';
|
||||
import { Document } from '../persistence/saved_object_store';
|
||||
import { Datasource, DatasourceMap } from '../types';
|
||||
|
||||
const defaultDoc: Document = {
|
||||
title: 'some-title',
|
||||
visualizationType: 'lnsXY',
|
||||
state: {
|
||||
query: {
|
||||
query: '',
|
||||
language: 'kuery',
|
||||
},
|
||||
visualization: {
|
||||
some: 'props',
|
||||
},
|
||||
datasourceStates: {
|
||||
indexpattern: {},
|
||||
},
|
||||
filters: [
|
||||
{
|
||||
meta: {
|
||||
index: 'reference-1',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
references: [
|
||||
{
|
||||
name: 'reference-1',
|
||||
id: 'id-1',
|
||||
type: 'index-pattern',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
describe('lens document equality', () => {
|
||||
const mockInjectFilterReferences = jest.fn((filters: Filter[]) =>
|
||||
filters.map((filter) => ({
|
||||
...filter,
|
||||
meta: {
|
||||
...filter.meta,
|
||||
index: 'injected!',
|
||||
},
|
||||
}))
|
||||
);
|
||||
|
||||
let mockDatasourceMap: DatasourceMap;
|
||||
|
||||
beforeEach(() => {
|
||||
mockDatasourceMap = {
|
||||
indexpattern: { isEqual: jest.fn(() => true) },
|
||||
} as unknown as DatasourceMap;
|
||||
});
|
||||
|
||||
it('returns true when documents are equal', () => {
|
||||
expect(
|
||||
isLensEqual(defaultDoc, defaultDoc, mockInjectFilterReferences, mockDatasourceMap)
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('handles undefined documents', () => {
|
||||
expect(isLensEqual(undefined, undefined, mockInjectFilterReferences, {})).toBeTruthy();
|
||||
expect(isLensEqual(undefined, {} as Document, mockInjectFilterReferences, {})).toBeFalsy();
|
||||
expect(isLensEqual({} as Document, undefined, mockInjectFilterReferences, {})).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should compare visualization type', () => {
|
||||
expect(
|
||||
isLensEqual(
|
||||
defaultDoc,
|
||||
{ ...defaultDoc, visualizationType: 'other-type' },
|
||||
mockInjectFilterReferences,
|
||||
mockDatasourceMap
|
||||
)
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should compare the query', () => {
|
||||
expect(
|
||||
isLensEqual(
|
||||
defaultDoc,
|
||||
{
|
||||
...defaultDoc,
|
||||
state: {
|
||||
...defaultDoc.state,
|
||||
query: {
|
||||
query: 'foobar',
|
||||
language: 'kuery',
|
||||
},
|
||||
},
|
||||
},
|
||||
mockInjectFilterReferences,
|
||||
mockDatasourceMap
|
||||
)
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should compare the visualization state', () => {
|
||||
expect(
|
||||
isLensEqual(
|
||||
defaultDoc,
|
||||
{
|
||||
...defaultDoc,
|
||||
state: {
|
||||
...defaultDoc.state,
|
||||
visualization: {
|
||||
some: 'other-props',
|
||||
},
|
||||
},
|
||||
},
|
||||
mockInjectFilterReferences,
|
||||
mockDatasourceMap
|
||||
)
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
describe('comparing the datasources', () => {
|
||||
it('checks available datasources', () => {
|
||||
// add an extra datasource in one doc
|
||||
expect(
|
||||
isLensEqual(
|
||||
defaultDoc,
|
||||
{
|
||||
...defaultDoc,
|
||||
state: {
|
||||
...defaultDoc.state,
|
||||
datasourceStates: {
|
||||
...defaultDoc.state.datasourceStates,
|
||||
foodatasource: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
mockInjectFilterReferences,
|
||||
{ ...mockDatasourceMap, foodatasource: { isEqual: () => true } as unknown as Datasource }
|
||||
)
|
||||
).toBeFalsy();
|
||||
|
||||
// ordering of the datasource states shouldn't matter
|
||||
expect(
|
||||
isLensEqual(
|
||||
{
|
||||
...defaultDoc,
|
||||
state: {
|
||||
...defaultDoc.state,
|
||||
datasourceStates: {
|
||||
foodatasource: {}, // first
|
||||
...defaultDoc.state.datasourceStates,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
...defaultDoc,
|
||||
state: {
|
||||
...defaultDoc.state,
|
||||
datasourceStates: {
|
||||
...defaultDoc.state.datasourceStates,
|
||||
foodatasource: {}, // last
|
||||
},
|
||||
},
|
||||
},
|
||||
mockInjectFilterReferences,
|
||||
{ ...mockDatasourceMap, foodatasource: { isEqual: () => true } as unknown as Datasource }
|
||||
)
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('delegates internal datasource comparison', () => {
|
||||
// datasource's isEqual returns false
|
||||
(mockDatasourceMap.indexpattern.isEqual as jest.Mock).mockReturnValue(false);
|
||||
expect(
|
||||
isLensEqual(defaultDoc, defaultDoc, mockInjectFilterReferences, mockDatasourceMap)
|
||||
).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
it('should ignore pinned filters', () => {
|
||||
// ignores pinned filters
|
||||
const pinnedFilter: Filter = {
|
||||
$state: {
|
||||
store: FilterStateStore.GLOBAL_STATE,
|
||||
},
|
||||
meta: {},
|
||||
};
|
||||
|
||||
const filtersWithPinned = [...defaultDoc.state.filters, pinnedFilter];
|
||||
|
||||
expect(
|
||||
isLensEqual(
|
||||
defaultDoc,
|
||||
{ ...defaultDoc, state: { ...defaultDoc.state, filters: filtersWithPinned } },
|
||||
mockInjectFilterReferences,
|
||||
mockDatasourceMap
|
||||
)
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should inject filter references', () => {
|
||||
// injects filter references for comparison
|
||||
expect(
|
||||
isLensEqual(
|
||||
defaultDoc,
|
||||
{
|
||||
...defaultDoc,
|
||||
state: {
|
||||
...defaultDoc.state,
|
||||
filters: [
|
||||
{
|
||||
meta: {
|
||||
index: 'some-other-reference-name',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
mockInjectFilterReferences,
|
||||
mockDatasourceMap
|
||||
)
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should consider undefined props equivalent to non-existant props', () => {
|
||||
expect(
|
||||
isLensEqual(
|
||||
defaultDoc,
|
||||
{
|
||||
...defaultDoc,
|
||||
state: {
|
||||
...defaultDoc.state,
|
||||
visualization: {
|
||||
...(defaultDoc.state.visualization as object),
|
||||
foo: undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
mockInjectFilterReferences,
|
||||
mockDatasourceMap
|
||||
)
|
||||
).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { isEqual, intersection, union } from 'lodash';
|
||||
import { FilterManager } from 'src/plugins/data/public';
|
||||
import { Document } from '../persistence/saved_object_store';
|
||||
import { DatasourceMap } from '../types';
|
||||
import { injectDocFilterReferences, removePinnedFilters } from './save_modal_container';
|
||||
|
||||
const removeNonSerializable = (obj: Parameters<JSON['stringify']>[0]) =>
|
||||
JSON.parse(JSON.stringify(obj));
|
||||
|
||||
export const isLensEqual = (
|
||||
doc1In: Document | undefined,
|
||||
doc2In: Document | undefined,
|
||||
injectFilterReferences: FilterManager['inject'],
|
||||
datasourceMap: DatasourceMap
|
||||
) => {
|
||||
if (doc1In === undefined || doc2In === undefined) {
|
||||
return doc1In === doc2In;
|
||||
}
|
||||
|
||||
// we do this so that undefined props are the same as non-existant props
|
||||
const doc1 = removeNonSerializable(doc1In);
|
||||
const doc2 = removeNonSerializable(doc2In);
|
||||
|
||||
if (doc1?.visualizationType !== doc2?.visualizationType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isEqual(doc1.state.query, doc2.state.query)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isEqual(doc1.state.visualization, doc2.state.visualization)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// data source equality
|
||||
const availableDatasourceTypes1 = Object.keys(doc1.state.datasourceStates);
|
||||
const availableDatasourceTypes2 = Object.keys(doc2.state.datasourceStates);
|
||||
|
||||
let datasourcesEqual =
|
||||
intersection(availableDatasourceTypes1, availableDatasourceTypes2).length ===
|
||||
union(availableDatasourceTypes1, availableDatasourceTypes2).length;
|
||||
|
||||
if (datasourcesEqual) {
|
||||
// equal so far, so actually check
|
||||
datasourcesEqual = availableDatasourceTypes1
|
||||
.map((type) =>
|
||||
datasourceMap[type].isEqual(
|
||||
doc1.state.datasourceStates[type],
|
||||
doc1.references,
|
||||
doc2.state.datasourceStates[type],
|
||||
doc2.references
|
||||
)
|
||||
)
|
||||
.every((res) => res);
|
||||
}
|
||||
|
||||
if (!datasourcesEqual) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const [filtersInjected1, filtersInjected2] = [doc1, doc2].map((doc) =>
|
||||
removePinnedFilters(injectDocFilterReferences(injectFilterReferences, doc))
|
||||
);
|
||||
if (!isEqual(filtersInjected1?.state.filters, filtersInjected2?.state.filters)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
|
@ -8,15 +8,14 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { METRIC_TYPE } from '@kbn/analytics';
|
||||
import { partition } from 'lodash';
|
||||
|
||||
import type { SavedObjectReference } from 'kibana/public';
|
||||
import { SaveModal } from './save_modal';
|
||||
import type { LensAppProps, LensAppServices } from './types';
|
||||
import type { SaveProps } from './app';
|
||||
import { Document, injectFilterReferences, checkForDuplicateTitle } from '../persistence';
|
||||
import { Document, checkForDuplicateTitle } from '../persistence';
|
||||
import type { LensByReferenceInput, LensEmbeddableInput } from '../embeddable';
|
||||
import { esFilters } from '../../../../../src/plugins/data/public';
|
||||
import { esFilters, FilterManager } from '../../../../../src/plugins/data/public';
|
||||
import { APP_ID, getFullPath, LENS_EMBEDDABLE_TYPE } from '../../common';
|
||||
import { trackUiEvent } from '../lens_ui_telemetry';
|
||||
import type { LensAppState } from '../state_management';
|
||||
|
@ -169,10 +168,11 @@ const redirectToDashboard = ({
|
|||
const getDocToSave = (
|
||||
lastKnownDoc: Document,
|
||||
saveProps: SaveProps,
|
||||
references: SavedObjectReference[]
|
||||
references: SavedObjectReference[],
|
||||
injectFilterReferences: FilterManager['inject']
|
||||
) => {
|
||||
const docToSave = {
|
||||
...getLastKnownDocWithoutPinnedFilters(lastKnownDoc)!,
|
||||
...injectDocFilterReferences(injectFilterReferences, removePinnedFilters(lastKnownDoc))!,
|
||||
references,
|
||||
};
|
||||
|
||||
|
@ -200,6 +200,7 @@ export const runSaveLensVisualization = async (
|
|||
): Promise<Partial<LensAppState> | undefined> => {
|
||||
const {
|
||||
chrome,
|
||||
data,
|
||||
initialInput,
|
||||
originatingApp,
|
||||
lastKnownDoc,
|
||||
|
@ -240,7 +241,12 @@ export const runSaveLensVisualization = async (
|
|||
);
|
||||
}
|
||||
|
||||
const docToSave = getDocToSave(lastKnownDoc, saveProps, references);
|
||||
const docToSave = getDocToSave(
|
||||
lastKnownDoc,
|
||||
saveProps,
|
||||
references,
|
||||
data.query.filterManager.inject
|
||||
);
|
||||
|
||||
// Required to serialize filters in by value mode until
|
||||
// https://github.com/elastic/kibana/issues/77588 is fixed
|
||||
|
@ -351,21 +357,29 @@ export const runSaveLensVisualization = async (
|
|||
}
|
||||
};
|
||||
|
||||
export function getLastKnownDocWithoutPinnedFilters(doc?: Document) {
|
||||
export function injectDocFilterReferences(
|
||||
injectFilterReferences: FilterManager['inject'],
|
||||
doc?: Document
|
||||
) {
|
||||
if (!doc) return undefined;
|
||||
const [pinnedFilters, appFilters] = partition(
|
||||
injectFilterReferences(doc.state?.filters || [], doc.references),
|
||||
esFilters.isFilterPinned
|
||||
);
|
||||
return pinnedFilters?.length
|
||||
? {
|
||||
...doc,
|
||||
state: {
|
||||
...doc.state,
|
||||
filters: appFilters,
|
||||
},
|
||||
}
|
||||
: doc;
|
||||
return {
|
||||
...doc,
|
||||
state: {
|
||||
...doc.state,
|
||||
filters: injectFilterReferences(doc.state?.filters || [], doc.references),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function removePinnedFilters(doc?: Document) {
|
||||
if (!doc) return undefined;
|
||||
return {
|
||||
...doc,
|
||||
state: {
|
||||
...doc.state,
|
||||
filters: (doc.state?.filters || []).filter((filter) => !esFilters.isFilterPinned(filter)),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
|
|
|
@ -17,7 +17,7 @@ import {
|
|||
import { ReactExpressionRendererProps } from 'src/plugins/expressions/public';
|
||||
import { spacesPluginMock } from '../../../spaces/public/mocks';
|
||||
import { Filter } from '@kbn/es-query';
|
||||
import { Query, TimeRange, IndexPatternsContract } from 'src/plugins/data/public';
|
||||
import { Query, TimeRange, IndexPatternsContract, FilterManager } from 'src/plugins/data/public';
|
||||
import { Document } from '../persistence';
|
||||
import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks';
|
||||
import { VIS_EVENT_TO_TRIGGER } from '../../../../../src/plugins/visualizations/public/embeddable';
|
||||
|
@ -72,6 +72,16 @@ const options = {
|
|||
checkForDuplicateTitle: defaultCheckForDuplicateTitle,
|
||||
};
|
||||
|
||||
const mockInjectFilterReferences: FilterManager['inject'] = (filters, references) => {
|
||||
return filters.map((filter) => ({
|
||||
...filter,
|
||||
meta: {
|
||||
...filter.meta,
|
||||
index: 'injected!',
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
const attributeServiceMockFromSavedVis = (document: Document): LensAttributeService => {
|
||||
const core = coreMock.createStart();
|
||||
const service = new AttributeService<
|
||||
|
@ -139,6 +149,7 @@ describe('embeddable', () => {
|
|||
getTrigger,
|
||||
theme: themeServiceMock.createStartContract(),
|
||||
visualizationMap: {},
|
||||
injectFilterReferences: jest.fn(mockInjectFilterReferences),
|
||||
documentToExpression: () =>
|
||||
Promise.resolve({
|
||||
ast: {
|
||||
|
@ -180,6 +191,7 @@ describe('embeddable', () => {
|
|||
capabilities: { canSaveDashboards: true, canSaveVisualizations: true },
|
||||
getTrigger,
|
||||
visualizationMap: {},
|
||||
injectFilterReferences: jest.fn(mockInjectFilterReferences),
|
||||
theme: themeServiceMock.createStartContract(),
|
||||
documentToExpression: () =>
|
||||
Promise.resolve({
|
||||
|
@ -226,6 +238,7 @@ describe('embeddable', () => {
|
|||
},
|
||||
getTrigger,
|
||||
visualizationMap: {},
|
||||
injectFilterReferences: jest.fn(mockInjectFilterReferences),
|
||||
theme: themeServiceMock.createStartContract(),
|
||||
documentToExpression: () =>
|
||||
Promise.resolve({
|
||||
|
@ -283,6 +296,7 @@ describe('embeddable', () => {
|
|||
},
|
||||
getTrigger,
|
||||
visualizationMap: {},
|
||||
injectFilterReferences: jest.fn(mockInjectFilterReferences),
|
||||
theme: themeServiceMock.createStartContract(),
|
||||
documentToExpression: () =>
|
||||
Promise.resolve({
|
||||
|
@ -329,6 +343,7 @@ describe('embeddable', () => {
|
|||
},
|
||||
getTrigger,
|
||||
visualizationMap: {},
|
||||
injectFilterReferences: jest.fn(mockInjectFilterReferences),
|
||||
theme: themeServiceMock.createStartContract(),
|
||||
documentToExpression: () =>
|
||||
Promise.resolve({
|
||||
|
@ -370,6 +385,7 @@ describe('embeddable', () => {
|
|||
},
|
||||
getTrigger,
|
||||
visualizationMap: {},
|
||||
injectFilterReferences: jest.fn(mockInjectFilterReferences),
|
||||
theme: themeServiceMock.createStartContract(),
|
||||
documentToExpression: () =>
|
||||
Promise.resolve({
|
||||
|
@ -414,6 +430,7 @@ describe('embeddable', () => {
|
|||
capabilities: { canSaveDashboards: true, canSaveVisualizations: true },
|
||||
getTrigger,
|
||||
visualizationMap: {},
|
||||
injectFilterReferences: jest.fn(mockInjectFilterReferences),
|
||||
theme: themeServiceMock.createStartContract(),
|
||||
documentToExpression: () =>
|
||||
Promise.resolve({
|
||||
|
@ -465,6 +482,7 @@ describe('embeddable', () => {
|
|||
},
|
||||
getTrigger,
|
||||
visualizationMap: {},
|
||||
injectFilterReferences: jest.fn(mockInjectFilterReferences),
|
||||
theme: themeServiceMock.createStartContract(),
|
||||
documentToExpression: () =>
|
||||
Promise.resolve({
|
||||
|
@ -514,6 +532,7 @@ describe('embeddable', () => {
|
|||
},
|
||||
getTrigger,
|
||||
visualizationMap: {},
|
||||
injectFilterReferences: jest.fn(mockInjectFilterReferences),
|
||||
theme: themeServiceMock.createStartContract(),
|
||||
documentToExpression: () =>
|
||||
Promise.resolve({
|
||||
|
@ -570,6 +589,7 @@ describe('embeddable', () => {
|
|||
},
|
||||
getTrigger,
|
||||
visualizationMap: {},
|
||||
injectFilterReferences: jest.fn(mockInjectFilterReferences),
|
||||
theme: themeServiceMock.createStartContract(),
|
||||
documentToExpression: () =>
|
||||
Promise.resolve({
|
||||
|
@ -592,7 +612,7 @@ describe('embeddable', () => {
|
|||
expect.objectContaining({
|
||||
timeRange,
|
||||
query: [query, savedVis.state.query],
|
||||
filters,
|
||||
filters: mockInjectFilterReferences(filters, []),
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -627,6 +647,7 @@ describe('embeddable', () => {
|
|||
},
|
||||
getTrigger,
|
||||
visualizationMap: {},
|
||||
injectFilterReferences: jest.fn(mockInjectFilterReferences),
|
||||
theme: themeServiceMock.createStartContract(),
|
||||
documentToExpression: () =>
|
||||
Promise.resolve({
|
||||
|
@ -663,9 +684,7 @@ describe('embeddable', () => {
|
|||
state: {
|
||||
...savedVis.state,
|
||||
query: { language: 'kquery', query: 'saved filter' },
|
||||
filters: [
|
||||
{ meta: { alias: 'test', negate: false, disabled: false, indexRefName: 'filter-0' } },
|
||||
],
|
||||
filters: [{ meta: { alias: 'test', negate: false, disabled: false, index: 'filter-0' } }],
|
||||
},
|
||||
references: [{ type: 'index-pattern', name: 'filter-0', id: 'my-index-pattern-id' }],
|
||||
};
|
||||
|
@ -687,6 +706,7 @@ describe('embeddable', () => {
|
|||
},
|
||||
getTrigger,
|
||||
visualizationMap: {},
|
||||
injectFilterReferences: jest.fn(mockInjectFilterReferences),
|
||||
theme: themeServiceMock.createStartContract(),
|
||||
documentToExpression: () =>
|
||||
Promise.resolve({
|
||||
|
@ -708,11 +728,14 @@ describe('embeddable', () => {
|
|||
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' } },
|
||||
],
|
||||
// actual index pattern id gets injected
|
||||
filters: mockInjectFilterReferences(
|
||||
[
|
||||
filters[0],
|
||||
{ meta: { alias: 'test', negate: false, disabled: false, index: 'injected!' } },
|
||||
],
|
||||
[]
|
||||
),
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -731,6 +754,7 @@ describe('embeddable', () => {
|
|||
},
|
||||
getTrigger,
|
||||
visualizationMap: {},
|
||||
injectFilterReferences: jest.fn(mockInjectFilterReferences),
|
||||
theme: themeServiceMock.createStartContract(),
|
||||
documentToExpression: () =>
|
||||
Promise.resolve({
|
||||
|
@ -775,6 +799,7 @@ describe('embeddable', () => {
|
|||
},
|
||||
getTrigger,
|
||||
visualizationMap: {},
|
||||
injectFilterReferences: jest.fn(mockInjectFilterReferences),
|
||||
theme: themeServiceMock.createStartContract(),
|
||||
documentToExpression: () =>
|
||||
Promise.resolve({
|
||||
|
@ -819,6 +844,7 @@ describe('embeddable', () => {
|
|||
},
|
||||
getTrigger,
|
||||
visualizationMap: {},
|
||||
injectFilterReferences: jest.fn(mockInjectFilterReferences),
|
||||
theme: themeServiceMock.createStartContract(),
|
||||
documentToExpression: () =>
|
||||
Promise.resolve({
|
||||
|
@ -878,6 +904,7 @@ describe('embeddable', () => {
|
|||
},
|
||||
getTrigger,
|
||||
visualizationMap: {},
|
||||
injectFilterReferences: jest.fn(mockInjectFilterReferences),
|
||||
theme: themeServiceMock.createStartContract(),
|
||||
documentToExpression: () =>
|
||||
Promise.resolve({
|
||||
|
@ -953,6 +980,7 @@ describe('embeddable', () => {
|
|||
},
|
||||
getTrigger,
|
||||
visualizationMap: {},
|
||||
injectFilterReferences: jest.fn(mockInjectFilterReferences),
|
||||
theme: themeServiceMock.createStartContract(),
|
||||
documentToExpression: () =>
|
||||
Promise.resolve({
|
||||
|
@ -1003,6 +1031,7 @@ describe('embeddable', () => {
|
|||
},
|
||||
getTrigger,
|
||||
visualizationMap: {},
|
||||
injectFilterReferences: jest.fn(mockInjectFilterReferences),
|
||||
theme: themeServiceMock.createStartContract(),
|
||||
documentToExpression: () =>
|
||||
Promise.resolve({
|
||||
|
@ -1053,6 +1082,7 @@ describe('embeddable', () => {
|
|||
},
|
||||
getTrigger,
|
||||
visualizationMap: {},
|
||||
injectFilterReferences: jest.fn(mockInjectFilterReferences),
|
||||
theme: themeServiceMock.createStartContract(),
|
||||
documentToExpression: () =>
|
||||
Promise.resolve({
|
||||
|
@ -1124,6 +1154,7 @@ describe('embeddable', () => {
|
|||
},
|
||||
getTrigger,
|
||||
theme: themeServiceMock.createStartContract(),
|
||||
injectFilterReferences: jest.fn(mockInjectFilterReferences),
|
||||
visualizationMap: {
|
||||
[visDocument.visualizationType as string]: {
|
||||
onEditAction: onEditActionMock,
|
||||
|
|
|
@ -16,6 +16,7 @@ import type {
|
|||
TimefilterContract,
|
||||
TimeRange,
|
||||
IndexPattern,
|
||||
FilterManager,
|
||||
} from 'src/plugins/data/public';
|
||||
import type { PaletteOutput } from 'src/plugins/charts/public';
|
||||
import type { Start as InspectorStart } from 'src/plugins/inspector/public';
|
||||
|
@ -42,7 +43,7 @@ import {
|
|||
SavedObjectEmbeddableInput,
|
||||
ReferenceOrValueEmbeddable,
|
||||
} from '../../../../../src/plugins/embeddable/public';
|
||||
import { Document, injectFilterReferences } from '../persistence';
|
||||
import { Document } from '../persistence';
|
||||
import { ExpressionWrapper, ExpressionWrapperProps } from './expression_wrapper';
|
||||
import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public';
|
||||
import {
|
||||
|
@ -107,6 +108,7 @@ export interface LensEmbeddableDeps {
|
|||
documentToExpression: (
|
||||
doc: Document
|
||||
) => Promise<{ ast: Ast | null; errors: ErrorMessage[] | undefined }>;
|
||||
injectFilterReferences: FilterManager['inject'];
|
||||
visualizationMap: VisualizationMap;
|
||||
indexPatternService: IndexPatternsContract;
|
||||
expressionRenderer: ReactExpressionRendererType;
|
||||
|
@ -477,7 +479,7 @@ export class Embeddable
|
|||
output.filters = [...this.savedVis.state.filters];
|
||||
}
|
||||
|
||||
output.filters = injectFilterReferences(output.filters, this.savedVis.references);
|
||||
output.filters = this.deps.injectFilterReferences(output.filters, this.savedVis.references);
|
||||
return output;
|
||||
}
|
||||
|
||||
|
|
|
@ -10,7 +10,11 @@ import { i18n } from '@kbn/i18n';
|
|||
import { RecursiveReadonly } from '@kbn/utility-types';
|
||||
import { Ast } from '@kbn/interpreter';
|
||||
import { UsageCollectionSetup } from 'src/plugins/usage_collection/public';
|
||||
import { IndexPatternsContract, TimefilterContract } from '../../../../../src/plugins/data/public';
|
||||
import {
|
||||
FilterManager,
|
||||
IndexPatternsContract,
|
||||
TimefilterContract,
|
||||
} from '../../../../../src/plugins/data/public';
|
||||
import { ReactExpressionRendererType } from '../../../../../src/plugins/expressions/public';
|
||||
import {
|
||||
EmbeddableFactoryDefinition,
|
||||
|
@ -40,6 +44,7 @@ export interface LensEmbeddableStartServices {
|
|||
documentToExpression: (
|
||||
doc: Document
|
||||
) => Promise<{ ast: Ast | null; errors: ErrorMessage[] | undefined }>;
|
||||
injectFilterReferences: FilterManager['inject'];
|
||||
visualizationMap: VisualizationMap;
|
||||
spaces?: SpacesPluginStart;
|
||||
theme: ThemeServiceStart;
|
||||
|
@ -88,6 +93,7 @@ export class EmbeddableFactory implements EmbeddableFactoryDefinition {
|
|||
timefilter,
|
||||
expressionRenderer,
|
||||
documentToExpression,
|
||||
injectFilterReferences,
|
||||
visualizationMap,
|
||||
uiActions,
|
||||
coreHttp,
|
||||
|
@ -113,6 +119,7 @@ export class EmbeddableFactory implements EmbeddableFactoryDefinition {
|
|||
getTrigger: uiActions?.getTrigger,
|
||||
getTriggerCompatibleActions: uiActions?.getTriggerCompatibleActions,
|
||||
documentToExpression,
|
||||
injectFilterReferences,
|
||||
visualizationMap,
|
||||
capabilities: {
|
||||
canSaveDashboards: Boolean(capabilities.dashboard?.showWriteControls),
|
||||
|
|
|
@ -28,3 +28,5 @@ export function loadInitialState() {
|
|||
const originalLoader = jest.requireActual('../loader');
|
||||
|
||||
export const extractReferences = originalLoader.extractReferences;
|
||||
|
||||
export const injectReferences = originalLoader.injectReferences;
|
||||
|
|
|
@ -29,6 +29,8 @@ import { indexPatternFieldEditorPluginMock } from 'src/plugins/data_view_field_e
|
|||
import { uiActionsPluginMock } from '../../../../../src/plugins/ui_actions/public/mocks';
|
||||
import { fieldFormatsServiceMock } from '../../../../../src/plugins/field_formats/public/mocks';
|
||||
import { TinymathAST } from 'packages/kbn-tinymath';
|
||||
import { SavedObjectReference } from 'kibana/server';
|
||||
import { cloneDeep } from 'lodash';
|
||||
|
||||
jest.mock('./loader');
|
||||
jest.mock('../id_generator');
|
||||
|
@ -1737,4 +1739,77 @@ describe('IndexPattern Data Source', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#isEqual', () => {
|
||||
const layerId = '8bd66b66-aba3-49fb-9ff2-4bf83f2be08e';
|
||||
|
||||
const persistableState: IndexPatternPersistedState = {
|
||||
layers: {
|
||||
[layerId]: {
|
||||
columns: {
|
||||
'fa649155-d7f5-49d9-af26-508287431244': {
|
||||
label: 'Count of records',
|
||||
dataType: 'number',
|
||||
operationType: 'count',
|
||||
isBucketed: false,
|
||||
scale: 'ratio',
|
||||
sourceField: 'Records',
|
||||
},
|
||||
},
|
||||
columnOrder: ['fa649155-d7f5-49d9-af26-508287431244'],
|
||||
incompleteColumns: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const currentIndexPatternReference = {
|
||||
id: 'some-id',
|
||||
name: 'indexpattern-datasource-current-indexpattern',
|
||||
type: 'index-pattern',
|
||||
};
|
||||
|
||||
const references1: SavedObjectReference[] = [
|
||||
currentIndexPatternReference,
|
||||
{
|
||||
id: 'some-id',
|
||||
name: 'indexpattern-datasource-layer-8bd66b66-aba3-49fb-9ff2-4bf83f2be08e',
|
||||
type: 'index-pattern',
|
||||
},
|
||||
];
|
||||
|
||||
const references2: SavedObjectReference[] = [
|
||||
currentIndexPatternReference,
|
||||
{
|
||||
id: 'some-DIFFERENT-id',
|
||||
name: 'indexpattern-datasource-layer-8bd66b66-aba3-49fb-9ff2-4bf83f2be08e',
|
||||
type: 'index-pattern',
|
||||
},
|
||||
];
|
||||
|
||||
it('should be false if datasource states are using different data views', () => {
|
||||
expect(
|
||||
indexPatternDatasource.isEqual(persistableState, references1, persistableState, references2)
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should be false if datasource states differ', () => {
|
||||
const differentPersistableState = cloneDeep(persistableState);
|
||||
differentPersistableState.layers[layerId].columnOrder = ['something else'];
|
||||
|
||||
expect(
|
||||
indexPatternDatasource.isEqual(
|
||||
persistableState,
|
||||
references1,
|
||||
differentPersistableState,
|
||||
references1
|
||||
)
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should be true if datasource states are identical and they refer to the same data view', () => {
|
||||
expect(
|
||||
indexPatternDatasource.isEqual(persistableState, references1, persistableState, references1)
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -12,6 +12,7 @@ import type { CoreStart, SavedObjectReference } from 'kibana/public';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import type { IStorageWrapper } from 'src/plugins/kibana_utils/public';
|
||||
import type { FieldFormatsStart } from 'src/plugins/field_formats/public';
|
||||
import { isEqual } from 'lodash';
|
||||
import type { IndexPatternFieldEditorStart } from '../../../../../src/plugins/data_view_field_editor/public';
|
||||
import type {
|
||||
DatasourceDimensionEditorProps,
|
||||
|
@ -27,6 +28,7 @@ import {
|
|||
changeIndexPattern,
|
||||
changeLayerIndexPattern,
|
||||
extractReferences,
|
||||
injectReferences,
|
||||
} from './loader';
|
||||
import { toExpression } from './to_expression';
|
||||
import {
|
||||
|
@ -545,6 +547,16 @@ export function getIndexPatternDatasource({
|
|||
})
|
||||
);
|
||||
},
|
||||
isEqual: (
|
||||
persistableState1: IndexPatternPersistedState,
|
||||
references1: SavedObjectReference[],
|
||||
persistableState2: IndexPatternPersistedState,
|
||||
references2: SavedObjectReference[]
|
||||
) =>
|
||||
isEqual(
|
||||
injectReferences(persistableState1, references1),
|
||||
injectReferences(persistableState2, references2)
|
||||
),
|
||||
};
|
||||
|
||||
return indexPatternDatasource;
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import { Observable, Subject } from 'rxjs';
|
||||
import moment from 'moment';
|
||||
import { DataPublicPluginStart, esFilters } from '../../../../../src/plugins/data/public';
|
||||
import { DataPublicPluginStart, esFilters, Filter } from '../../../../../src/plugins/data/public';
|
||||
|
||||
function createMockTimefilter() {
|
||||
const unsubscribe = jest.fn();
|
||||
|
@ -90,6 +90,19 @@ export function mockDataPlugin(
|
|||
filters = [];
|
||||
subscriber();
|
||||
},
|
||||
inject: (filtersIn: Filter[]) => {
|
||||
return filtersIn.map((filter) => ({
|
||||
...filter,
|
||||
meta: { ...filter.meta, index: 'injected!' },
|
||||
}));
|
||||
},
|
||||
extract: (filtersIn: Filter[]) => {
|
||||
const state = filtersIn.map((filter) => ({
|
||||
...filter,
|
||||
meta: { ...filter.meta, index: 'extracted!' },
|
||||
}));
|
||||
return { state, references: [] };
|
||||
},
|
||||
};
|
||||
}
|
||||
function createMockQueryString() {
|
||||
|
|
|
@ -51,6 +51,7 @@ export function createMockDatasource(id: string): DatasourceMock {
|
|||
checkIntegrity: jest.fn((_state) => []),
|
||||
isTimeBased: jest.fn(),
|
||||
isValidColumn: jest.fn(),
|
||||
isEqual: jest.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -40,7 +40,7 @@ export const defaultDoc = {
|
|||
visualizationType: 'testVis',
|
||||
state: {
|
||||
query: 'kuery',
|
||||
filters: [{ query: { match_phrase: { src: 'test' } } }],
|
||||
filters: [{ query: { match_phrase: { src: 'test' } }, meta: { index: 'index-pattern-0' } }],
|
||||
datasourceStates: {
|
||||
testDatasource: 'datasource',
|
||||
},
|
||||
|
|
|
@ -1,112 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { Filter } from '@kbn/es-query';
|
||||
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 remove index and value from persistable filter', () => {
|
||||
const { persistableFilters } = extractFilterReferences([
|
||||
{ ...filters[0], meta: { ...filters[0].meta, value: 'CN' } },
|
||||
{ ...filters[1], meta: { ...filters[1].meta, value: 'US' } },
|
||||
]);
|
||||
expect(persistableFilters.length).toBe(2);
|
||||
persistableFilters.forEach((filter) => {
|
||||
expect(filter.meta.hasOwnProperty('index')).toBe(false);
|
||||
expect(filter.meta.hasOwnProperty('value')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
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',
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -1,62 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { Filter } from '@kbn/es-query';
|
||||
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,
|
||||
});
|
||||
const newFilter = {
|
||||
...filterRow,
|
||||
meta: {
|
||||
...filterRow.meta,
|
||||
indexRefName: refName,
|
||||
},
|
||||
};
|
||||
// remove index because it's specified by indexRefName
|
||||
delete newFilter.meta.index;
|
||||
// remove value because it can't be persisted
|
||||
delete newFilter.meta.value;
|
||||
return newFilter;
|
||||
});
|
||||
|
||||
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 },
|
||||
};
|
||||
});
|
||||
}
|
|
@ -6,5 +6,4 @@
|
|||
*/
|
||||
|
||||
export * from './saved_object_store';
|
||||
export * from './filter_references';
|
||||
export { checkForDuplicateTitle } from './saved_objects_utils';
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { Filter } from '@kbn/es-query';
|
||||
import {
|
||||
SavedObjectAttributes,
|
||||
SavedObjectsClientContract,
|
||||
|
@ -12,7 +13,7 @@ import {
|
|||
ResolvedSimpleSavedObject,
|
||||
} from 'kibana/public';
|
||||
import { Query } from '../../../../../src/plugins/data/public';
|
||||
import { DOC_TYPE, PersistableFilter } from '../../common';
|
||||
import { DOC_TYPE } from '../../common';
|
||||
import { LensSavedObjectAttributes } from '../async_services';
|
||||
|
||||
export interface Document {
|
||||
|
@ -29,7 +30,7 @@ export interface Document {
|
|||
activePaletteId: string;
|
||||
state?: unknown;
|
||||
};
|
||||
filters: PersistableFilter[];
|
||||
filters: Filter[];
|
||||
};
|
||||
references: SavedObjectReference[];
|
||||
}
|
||||
|
|
|
@ -212,6 +212,7 @@ export class LensPlugin {
|
|||
timefilter: plugins.data.query.timefilter.timefilter,
|
||||
expressionRenderer: plugins.expressions.ReactExpressionRenderer,
|
||||
documentToExpression: this.editorFrameService!.documentToExpression,
|
||||
injectFilterReferences: data.query.filterManager.inject,
|
||||
visualizationMap,
|
||||
indexPatternService: plugins.data.indexPatterns,
|
||||
uiActions: plugins.uiActions,
|
||||
|
|
|
@ -37,6 +37,9 @@ Object {
|
|||
},
|
||||
"filters": Array [
|
||||
Object {
|
||||
"meta": Object {
|
||||
"index": "index-pattern-0",
|
||||
},
|
||||
"query": Object {
|
||||
"match_phrase": Object {
|
||||
"src": "test",
|
||||
|
|
|
@ -16,7 +16,7 @@ import { getInitialDatasourceId } from '../../utils';
|
|||
import { initializeDatasources } from '../../editor_frame_service/editor_frame';
|
||||
import { LensAppServices } from '../../app_plugin/types';
|
||||
import { getEditPath, getFullPath, LENS_EMBEDDABLE_TYPE } from '../../../common/constants';
|
||||
import { Document, injectFilterReferences } from '../../persistence';
|
||||
import { Document } from '../../persistence';
|
||||
|
||||
export const getPersisted = async ({
|
||||
initialInput,
|
||||
|
@ -162,7 +162,7 @@ export function loadInitial(
|
|||
{}
|
||||
);
|
||||
|
||||
const filters = injectFilterReferences(doc.state.filters, doc.references);
|
||||
const filters = data.query.filterManager.inject(doc.state.filters, doc.references);
|
||||
// Don't overwrite any pinned filters
|
||||
data.query.filterManager.setAppFilters(filters);
|
||||
|
||||
|
|
|
@ -224,7 +224,7 @@ describe('Initializing the store', () => {
|
|||
});
|
||||
|
||||
expect(deps.lensServices.data.query.filterManager.setAppFilters).toHaveBeenCalledWith([
|
||||
{ query: { match_phrase: { src: 'test' } } },
|
||||
{ query: { match_phrase: { src: 'test' } }, meta: { index: 'injected!' } },
|
||||
]);
|
||||
|
||||
expect(store.getState()).toEqual({
|
||||
|
|
|
@ -7,8 +7,8 @@
|
|||
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { SavedObjectReference } from 'kibana/server';
|
||||
import { FilterManager } from 'src/plugins/data/public';
|
||||
import { LensState } from './types';
|
||||
import { extractFilterReferences } from '../persistence';
|
||||
import { Datasource, DatasourceMap, VisualizationMap } from '../types';
|
||||
import { getDatasourceLayers } from '../editor_frame_service/editor_frame';
|
||||
|
||||
|
@ -43,13 +43,10 @@ export const selectExecutionContextSearch = createSelector(selectExecutionContex
|
|||
filters: res.filters,
|
||||
}));
|
||||
|
||||
const selectDatasourceMap = (state: LensState, datasourceMap: DatasourceMap) => datasourceMap;
|
||||
const selectInjectedDependencies = (_state: LensState, dependencies: unknown) => dependencies;
|
||||
|
||||
const selectVisualizationMap = (
|
||||
state: LensState,
|
||||
datasourceMap: DatasourceMap,
|
||||
visualizationMap: VisualizationMap
|
||||
) => visualizationMap;
|
||||
// use this type to cast selectInjectedDependencies to require whatever outside dependencies the selector needs
|
||||
type SelectInjectedDependenciesFunction<T> = (state: LensState, dependencies: T) => T;
|
||||
|
||||
export const selectSavedObjectFormat = createSelector(
|
||||
[
|
||||
|
@ -59,8 +56,11 @@ export const selectSavedObjectFormat = createSelector(
|
|||
selectQuery,
|
||||
selectFilters,
|
||||
selectActiveDatasourceId,
|
||||
selectDatasourceMap,
|
||||
selectVisualizationMap,
|
||||
selectInjectedDependencies as SelectInjectedDependenciesFunction<{
|
||||
datasourceMap: DatasourceMap;
|
||||
visualizationMap: VisualizationMap;
|
||||
extractFilterReferences: FilterManager['extract'];
|
||||
}>,
|
||||
],
|
||||
(
|
||||
persistedDoc,
|
||||
|
@ -69,8 +69,7 @@ export const selectSavedObjectFormat = createSelector(
|
|||
query,
|
||||
filters,
|
||||
activeDatasourceId,
|
||||
datasourceMap,
|
||||
visualizationMap
|
||||
{ datasourceMap, visualizationMap, extractFilterReferences }
|
||||
) => {
|
||||
const activeVisualization =
|
||||
visualization.state && visualization.activeId && visualizationMap[visualization.activeId];
|
||||
|
@ -101,7 +100,8 @@ export const selectSavedObjectFormat = createSelector(
|
|||
references.push(...savedObjectReferences);
|
||||
});
|
||||
|
||||
const { persistableFilters, references: filterReferences } = extractFilterReferences(filters);
|
||||
const { state: persistableFilters, references: filterReferences } =
|
||||
extractFilterReferences(filters);
|
||||
|
||||
references.push(...filterReferences);
|
||||
|
||||
|
@ -140,12 +140,19 @@ export const selectAreDatasourcesLoaded = createSelector(
|
|||
);
|
||||
|
||||
export const selectDatasourceLayers = createSelector(
|
||||
[selectDatasourceStates, selectDatasourceMap],
|
||||
[
|
||||
selectDatasourceStates,
|
||||
selectInjectedDependencies as SelectInjectedDependenciesFunction<DatasourceMap>,
|
||||
],
|
||||
(datasourceStates, datasourceMap) => getDatasourceLayers(datasourceStates, datasourceMap)
|
||||
);
|
||||
|
||||
export const selectFramePublicAPI = createSelector(
|
||||
[selectDatasourceStates, selectActiveData, selectDatasourceMap],
|
||||
[
|
||||
selectDatasourceStates,
|
||||
selectActiveData,
|
||||
selectInjectedDependencies as SelectInjectedDependenciesFunction<DatasourceMap>,
|
||||
],
|
||||
(datasourceStates, activeData, datasourceMap) => {
|
||||
return {
|
||||
datasourceLayers: getDatasourceLayers(datasourceStates, datasourceMap),
|
||||
|
|
|
@ -289,6 +289,15 @@ export interface Datasource<T = unknown, P = unknown> {
|
|||
* Given the current state layer and a columnId will verify if the column configuration has errors
|
||||
*/
|
||||
isValidColumn: (state: T, layerId: string, columnId: string) => boolean;
|
||||
/**
|
||||
* Are these datasources equivalent?
|
||||
*/
|
||||
isEqual: (
|
||||
persistableState1: P,
|
||||
references1: SavedObjectReference[],
|
||||
persistableState2: P,
|
||||
references2: SavedObjectReference[]
|
||||
) => boolean;
|
||||
}
|
||||
|
||||
export interface DatasourceFixAction<T> {
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import semverGte from 'semver/functions/gte';
|
||||
import { lensEmbeddableFactory } from './lens_embeddable_factory';
|
||||
import { migrations } from '../migrations/saved_object_migrations';
|
||||
|
||||
describe('saved object migrations and embeddable migrations', () => {
|
||||
test('should have same versions registered (>7.13.0)', () => {
|
||||
const savedObjectMigrationVersions = Object.keys(migrations).filter((version) => {
|
||||
return semverGte(version, '7.13.1');
|
||||
});
|
||||
const embeddableMigrationVersions = lensEmbeddableFactory()?.migrations;
|
||||
if (embeddableMigrationVersions) {
|
||||
expect(savedObjectMigrationVersions.sort()).toEqual(
|
||||
Object.keys(embeddableMigrationVersions).sort()
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
|
@ -1,67 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EmbeddableRegistryDefinition } from 'src/plugins/embeddable/server';
|
||||
import type { SerializableRecord } from '@kbn/utility-types';
|
||||
import { DOC_TYPE } from '../../common';
|
||||
import {
|
||||
commonMakeReversePaletteAsCustom,
|
||||
commonRemoveTimezoneDateHistogramParam,
|
||||
commonRenameOperationsForFormula,
|
||||
commonUpdateVisLayerType,
|
||||
} from '../migrations/common_migrations';
|
||||
import {
|
||||
LensDocShape713,
|
||||
LensDocShape715,
|
||||
LensDocShapePre712,
|
||||
VisState716,
|
||||
VisStatePre715,
|
||||
} from '../migrations/types';
|
||||
import { extract, inject } from '../../common/embeddable_factory';
|
||||
|
||||
export const lensEmbeddableFactory = (): EmbeddableRegistryDefinition => {
|
||||
return {
|
||||
id: DOC_TYPE,
|
||||
migrations: {
|
||||
// This migration is run in 7.13.1 for `by value` panels because the 7.13 release window was missed.
|
||||
'7.13.1': (state) => {
|
||||
const lensState = state as unknown as { attributes: LensDocShapePre712 };
|
||||
const migratedLensState = commonRenameOperationsForFormula(lensState.attributes);
|
||||
return {
|
||||
...lensState,
|
||||
attributes: migratedLensState,
|
||||
} as unknown as SerializableRecord;
|
||||
},
|
||||
'7.14.0': (state) => {
|
||||
const lensState = state as unknown as { attributes: LensDocShape713 };
|
||||
const migratedLensState = commonRemoveTimezoneDateHistogramParam(lensState.attributes);
|
||||
return {
|
||||
...lensState,
|
||||
attributes: migratedLensState,
|
||||
} as unknown as SerializableRecord;
|
||||
},
|
||||
'7.15.0': (state) => {
|
||||
const lensState = state as unknown as { attributes: LensDocShape715<VisStatePre715> };
|
||||
const migratedLensState = commonUpdateVisLayerType(lensState.attributes);
|
||||
return {
|
||||
...lensState,
|
||||
attributes: migratedLensState,
|
||||
} as unknown as SerializableRecord;
|
||||
},
|
||||
'7.16.0': (state) => {
|
||||
const lensState = state as unknown as { attributes: LensDocShape715<VisState716> };
|
||||
const migratedLensState = commonMakeReversePaletteAsCustom(lensState.attributes);
|
||||
return {
|
||||
...lensState,
|
||||
attributes: migratedLensState,
|
||||
} as unknown as SerializableRecord;
|
||||
},
|
||||
},
|
||||
extract,
|
||||
inject,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import semverGte from 'semver/functions/gte';
|
||||
import { makeLensEmbeddableFactory } from './make_lens_embeddable_factory';
|
||||
import { getAllMigrations } from '../migrations/saved_object_migrations';
|
||||
import { Filter } from '@kbn/es-query';
|
||||
|
||||
describe('embeddable migrations', () => {
|
||||
test('should have all saved object migrations versions (>7.13.0)', () => {
|
||||
const savedObjectMigrationVersions = Object.keys(getAllMigrations({})).filter((version) => {
|
||||
return semverGte(version, '7.13.1');
|
||||
});
|
||||
const embeddableMigrationVersions = makeLensEmbeddableFactory({})()?.migrations;
|
||||
if (embeddableMigrationVersions) {
|
||||
expect(savedObjectMigrationVersions.sort()).toEqual(
|
||||
Object.keys(embeddableMigrationVersions).sort()
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('should properly apply a filter migration within a lens visualization', () => {
|
||||
const migrationVersion = 'some-version';
|
||||
|
||||
const lensVisualizationDoc = {
|
||||
attributes: {
|
||||
state: {
|
||||
filters: [
|
||||
{
|
||||
filter: 1,
|
||||
migrated: false,
|
||||
},
|
||||
{
|
||||
filter: 2,
|
||||
migrated: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const embeddableMigrationVersions = makeLensEmbeddableFactory({
|
||||
[migrationVersion]: (filters: Filter[]) => {
|
||||
return filters.map((filterState) => ({
|
||||
...filterState,
|
||||
migrated: true,
|
||||
}));
|
||||
},
|
||||
})()?.migrations;
|
||||
|
||||
const migratedLensDoc = embeddableMigrationVersions?.[migrationVersion](lensVisualizationDoc);
|
||||
|
||||
expect(migratedLensDoc).toEqual({
|
||||
attributes: {
|
||||
state: {
|
||||
filters: [
|
||||
{
|
||||
filter: 1,
|
||||
migrated: true,
|
||||
},
|
||||
{
|
||||
filter: 2,
|
||||
migrated: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EmbeddableRegistryDefinition } from 'src/plugins/embeddable/server';
|
||||
import type { SerializableRecord } from '@kbn/utility-types';
|
||||
import {
|
||||
mergeMigrationFunctionMaps,
|
||||
MigrateFunctionsObject,
|
||||
} from '../../../../../src/plugins/kibana_utils/common';
|
||||
import { DOC_TYPE } from '../../common';
|
||||
import {
|
||||
commonMakeReversePaletteAsCustom,
|
||||
commonRemoveTimezoneDateHistogramParam,
|
||||
commonRenameFilterReferences,
|
||||
commonRenameOperationsForFormula,
|
||||
commonUpdateVisLayerType,
|
||||
getLensFilterMigrations,
|
||||
} from '../migrations/common_migrations';
|
||||
import {
|
||||
LensDocShape713,
|
||||
LensDocShape715,
|
||||
LensDocShapePre712,
|
||||
VisState716,
|
||||
VisStatePre715,
|
||||
} from '../migrations/types';
|
||||
import { extract, inject } from '../../common/embeddable_factory';
|
||||
|
||||
export const makeLensEmbeddableFactory =
|
||||
(filterMigrations: MigrateFunctionsObject) => (): EmbeddableRegistryDefinition => {
|
||||
return {
|
||||
id: DOC_TYPE,
|
||||
migrations: mergeMigrationFunctionMaps(getLensFilterMigrations(filterMigrations), {
|
||||
// This migration is run in 7.13.1 for `by value` panels because the 7.13 release window was missed.
|
||||
'7.13.1': (state) => {
|
||||
const lensState = state as unknown as { attributes: LensDocShapePre712 };
|
||||
const migratedLensState = commonRenameOperationsForFormula(lensState.attributes);
|
||||
return {
|
||||
...lensState,
|
||||
attributes: migratedLensState,
|
||||
} as unknown as SerializableRecord;
|
||||
},
|
||||
'7.14.0': (state) => {
|
||||
const lensState = state as unknown as { attributes: LensDocShape713 };
|
||||
const migratedLensState = commonRemoveTimezoneDateHistogramParam(lensState.attributes);
|
||||
return {
|
||||
...lensState,
|
||||
attributes: migratedLensState,
|
||||
} as unknown as SerializableRecord;
|
||||
},
|
||||
'7.15.0': (state) => {
|
||||
const lensState = state as unknown as { attributes: LensDocShape715<VisStatePre715> };
|
||||
const migratedLensState = commonUpdateVisLayerType(lensState.attributes);
|
||||
return {
|
||||
...lensState,
|
||||
attributes: migratedLensState,
|
||||
} as unknown as SerializableRecord;
|
||||
},
|
||||
'7.16.0': (state) => {
|
||||
const lensState = state as unknown as { attributes: LensDocShape715<VisState716> };
|
||||
const migratedLensState = commonMakeReversePaletteAsCustom(lensState.attributes);
|
||||
return {
|
||||
...lensState,
|
||||
attributes: migratedLensState,
|
||||
} as unknown as SerializableRecord;
|
||||
},
|
||||
'8.1.0': (state) => {
|
||||
const lensState = state as unknown as { attributes: LensDocShape715<VisState716> };
|
||||
const migratedLensState = commonRenameFilterReferences(lensState.attributes);
|
||||
return {
|
||||
...lensState,
|
||||
attributes: migratedLensState,
|
||||
} as unknown as SerializableRecord;
|
||||
},
|
||||
}),
|
||||
extract,
|
||||
inject,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { Filter } from '@kbn/es-query';
|
||||
import { getLensFilterMigrations } from './common_migrations';
|
||||
|
||||
describe('Lens migrations', () => {
|
||||
describe('applying filter migrations', () => {
|
||||
it('creates a filter migrations map that works on a lens visualization', () => {
|
||||
const filterMigrations = {
|
||||
'1.1': (filters: Filter[]) => filters.map((filter) => ({ ...filter, version: '1.1' })),
|
||||
'2.2': (filters: Filter[]) => filters.map((filter) => ({ ...filter, version: '2.2' })),
|
||||
'3.3': (filters: Filter[]) => filters.map((filter) => ({ ...filter, version: '3.3' })),
|
||||
};
|
||||
|
||||
const lensVisualizationSavedObject = {
|
||||
attributes: {
|
||||
state: {
|
||||
filters: [{}, {}],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const migrationMap = getLensFilterMigrations(filterMigrations);
|
||||
|
||||
expect(migrationMap['1.1'](lensVisualizationSavedObject).attributes.state.filters).toEqual([
|
||||
{ version: '1.1' },
|
||||
{ version: '1.1' },
|
||||
]);
|
||||
expect(migrationMap['2.2'](lensVisualizationSavedObject).attributes.state.filters).toEqual([
|
||||
{ version: '2.2' },
|
||||
{ version: '2.2' },
|
||||
]);
|
||||
expect(migrationMap['3.3'](lensVisualizationSavedObject).attributes.state.filters).toEqual([
|
||||
{ version: '3.3' },
|
||||
{ version: '3.3' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -7,6 +7,11 @@
|
|||
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { PaletteOutput } from 'src/plugins/charts/common';
|
||||
import { Filter } from '@kbn/es-query';
|
||||
import {
|
||||
MigrateFunction,
|
||||
MigrateFunctionsObject,
|
||||
} from '../../../../../src/plugins/kibana_utils/common';
|
||||
import {
|
||||
LensDocShapePre712,
|
||||
OperationTypePre712,
|
||||
|
@ -19,6 +24,7 @@ import {
|
|||
VisState716,
|
||||
} from './types';
|
||||
import { CustomPaletteParams, layerTypes } from '../../common';
|
||||
import { LensDocShape } from './saved_object_migrations';
|
||||
|
||||
export const commonRenameOperationsForFormula = (
|
||||
attributes: LensDocShapePre712
|
||||
|
@ -155,3 +161,40 @@ export const commonMakeReversePaletteAsCustom = (
|
|||
}
|
||||
return newAttributes;
|
||||
};
|
||||
|
||||
export const commonRenameFilterReferences = (attributes: LensDocShape715<VisState716>) => {
|
||||
const newAttributes = cloneDeep(attributes);
|
||||
for (const filter of newAttributes.state.filters) {
|
||||
filter.meta.index = filter.meta.indexRefName;
|
||||
delete filter.meta.indexRefName;
|
||||
}
|
||||
return newAttributes;
|
||||
};
|
||||
|
||||
const getApplyFilterMigrationToLens = (filterMigration: MigrateFunction<Filter[]>) => {
|
||||
return (savedObject: { attributes: LensDocShape }) => {
|
||||
return {
|
||||
...savedObject,
|
||||
attributes: {
|
||||
...savedObject.attributes,
|
||||
state: {
|
||||
...savedObject.attributes.state,
|
||||
filters: filterMigration(savedObject.attributes.state.filters as unknown as Filter[]),
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* This creates a migration map that applies filter migrations to Lens visualizations
|
||||
*/
|
||||
export const getLensFilterMigrations = (filterMigrations: MigrateFunctionsObject) => {
|
||||
const migrationMap: MigrateFunctionsObject = {};
|
||||
for (const version in filterMigrations) {
|
||||
if (filterMigrations.hasOwnProperty(version)) {
|
||||
migrationMap[version] = getApplyFilterMigrationToLens(filterMigrations[version]);
|
||||
}
|
||||
}
|
||||
return migrationMap;
|
||||
};
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { migrations, LensDocShape } from './saved_object_migrations';
|
||||
import { getAllMigrations, LensDocShape } from './saved_object_migrations';
|
||||
import {
|
||||
SavedObjectMigrationContext,
|
||||
SavedObjectMigrationFn,
|
||||
|
@ -15,8 +15,10 @@ import {
|
|||
import { LensDocShape715, VisState716, VisStatePost715, VisStatePre715 } from './types';
|
||||
import { CustomPaletteParams, layerTypes } from '../../common';
|
||||
import { PaletteOutput } from 'src/plugins/charts/common';
|
||||
import { Filter } from '@kbn/es-query';
|
||||
|
||||
describe('Lens migrations', () => {
|
||||
const migrations = getAllMigrations({});
|
||||
describe('7.7.0 missing dimensions in XY', () => {
|
||||
const context = {} as SavedObjectMigrationContext;
|
||||
|
||||
|
@ -1404,4 +1406,162 @@ describe('Lens migrations', () => {
|
|||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('8.1.0 update filter reference schema', () => {
|
||||
const context = { log: { warning: () => {} } } as unknown as SavedObjectMigrationContext;
|
||||
const example = {
|
||||
type: 'lens',
|
||||
id: 'mocked-saved-object-id',
|
||||
attributes: {
|
||||
savedObjectId: '1',
|
||||
title: 'MyRenamedOps',
|
||||
description: '',
|
||||
visualizationType: null,
|
||||
state: {
|
||||
datasourceMetaData: {
|
||||
filterableIndexPatterns: [],
|
||||
},
|
||||
datasourceStates: {
|
||||
indexpattern: {
|
||||
currentIndexPatternId: 'logstash-*',
|
||||
layers: {
|
||||
'2': {
|
||||
columns: {
|
||||
'3': {
|
||||
label: '@timestamp',
|
||||
dataType: 'date',
|
||||
operationType: 'date_histogram',
|
||||
sourceField: '@timestamp',
|
||||
isBucketed: true,
|
||||
scale: 'interval',
|
||||
params: { interval: 'auto', timeZone: 'Europe/Berlin' },
|
||||
},
|
||||
'4': {
|
||||
label: '@timestamp',
|
||||
dataType: 'date',
|
||||
operationType: 'date_histogram',
|
||||
sourceField: '@timestamp',
|
||||
isBucketed: true,
|
||||
scale: 'interval',
|
||||
params: { interval: 'auto' },
|
||||
},
|
||||
'5': {
|
||||
label: '@timestamp',
|
||||
dataType: 'date',
|
||||
operationType: 'my_unexpected_operation',
|
||||
isBucketed: true,
|
||||
scale: 'interval',
|
||||
params: { timeZone: 'do not delete' },
|
||||
},
|
||||
},
|
||||
columnOrder: ['3', '4', '5'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
visualization: {},
|
||||
query: { query: '', language: 'kuery' },
|
||||
filters: [
|
||||
{
|
||||
meta: {
|
||||
alias: null,
|
||||
negate: false,
|
||||
disabled: false,
|
||||
type: 'phrase',
|
||||
key: 'geo.src',
|
||||
params: { query: 'US' },
|
||||
indexRefName: 'filter-index-pattern-0',
|
||||
},
|
||||
query: { match_phrase: { 'geo.src': 'US' } },
|
||||
$state: { store: 'appState' },
|
||||
},
|
||||
{
|
||||
meta: {
|
||||
alias: null,
|
||||
negate: false,
|
||||
disabled: false,
|
||||
type: 'phrase',
|
||||
key: 'client_ip',
|
||||
params: { query: '1234.5344.2243.3245' },
|
||||
indexRefName: 'filter-index-pattern-2',
|
||||
},
|
||||
query: { match_phrase: { client_ip: '1234.5344.2243.3245' } },
|
||||
$state: { store: 'appState' },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
} as unknown as SavedObjectUnsanitizedDoc<LensDocShape715<VisState716>>;
|
||||
|
||||
it('should rename indexRefName to index in filters metadata', () => {
|
||||
const expectedFilters = example.attributes.state.filters.map((filter) => {
|
||||
return {
|
||||
...filter,
|
||||
meta: {
|
||||
...filter.meta,
|
||||
index: filter.meta.indexRefName,
|
||||
indexRefName: undefined,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const result = migrations['8.1.0'](example, context) as ReturnType<
|
||||
SavedObjectMigrationFn<LensDocShape, LensDocShape>
|
||||
>;
|
||||
|
||||
expect(result.attributes.state.filters).toEqual(expectedFilters);
|
||||
});
|
||||
});
|
||||
|
||||
test('should properly apply a filter migration within a lens visualization', () => {
|
||||
const migrationVersion = 'some-version';
|
||||
|
||||
const lensVisualizationDoc = {
|
||||
attributes: {
|
||||
state: {
|
||||
filters: [
|
||||
{
|
||||
filter: 1,
|
||||
migrated: false,
|
||||
},
|
||||
{
|
||||
filter: 2,
|
||||
migrated: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const migrationFunctionsObject = getAllMigrations({
|
||||
[migrationVersion]: (filters: Filter[]) => {
|
||||
return filters.map((filterState) => ({
|
||||
...filterState,
|
||||
migrated: true,
|
||||
}));
|
||||
},
|
||||
});
|
||||
|
||||
const migratedLensDoc = migrationFunctionsObject[migrationVersion](
|
||||
lensVisualizationDoc as SavedObjectUnsanitizedDoc,
|
||||
{} as SavedObjectMigrationContext
|
||||
);
|
||||
|
||||
expect(migratedLensDoc).toEqual({
|
||||
attributes: {
|
||||
state: {
|
||||
filters: [
|
||||
{
|
||||
filter: 1,
|
||||
migrated: true,
|
||||
},
|
||||
{
|
||||
filter: 2,
|
||||
migrated: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,16 +5,18 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { cloneDeep, mergeWith } from 'lodash';
|
||||
import { fromExpression, toExpression, Ast, ExpressionFunctionAST } from '@kbn/interpreter';
|
||||
import {
|
||||
SavedObjectMigrationMap,
|
||||
SavedObjectMigrationFn,
|
||||
SavedObjectReference,
|
||||
SavedObjectUnsanitizedDoc,
|
||||
SavedObjectMigrationContext,
|
||||
} from 'src/core/server';
|
||||
import { Filter } from '@kbn/es-query';
|
||||
import { Query } from 'src/plugins/data/public';
|
||||
import { MigrateFunctionsObject } from '../../../../../src/plugins/kibana_utils/common';
|
||||
import { PersistableFilter } from '../../common';
|
||||
import {
|
||||
LensDocShapePost712,
|
||||
|
@ -31,6 +33,8 @@ import {
|
|||
commonRemoveTimezoneDateHistogramParam,
|
||||
commonUpdateVisLayerType,
|
||||
commonMakeReversePaletteAsCustom,
|
||||
commonRenameFilterReferences,
|
||||
getLensFilterMigrations,
|
||||
} from './common_migrations';
|
||||
|
||||
interface LensDocShapePre710<VisualizationState = unknown> {
|
||||
|
@ -440,7 +444,15 @@ const moveDefaultReversedPaletteToCustom: SavedObjectMigrationFn<
|
|||
return { ...newDoc, attributes: commonMakeReversePaletteAsCustom(newDoc.attributes) };
|
||||
};
|
||||
|
||||
export const migrations: SavedObjectMigrationMap = {
|
||||
const renameFilterReferences: SavedObjectMigrationFn<
|
||||
LensDocShape715<VisState716>,
|
||||
LensDocShape715<VisState716>
|
||||
> = (doc) => {
|
||||
const newDoc = cloneDeep(doc);
|
||||
return { ...newDoc, attributes: commonRenameFilterReferences(newDoc.attributes) };
|
||||
};
|
||||
|
||||
const lensMigrations: 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).
|
||||
|
@ -453,4 +465,25 @@ export const migrations: SavedObjectMigrationMap = {
|
|||
'7.14.0': removeTimezoneDateHistogramParam,
|
||||
'7.15.0': addLayerTypeToVisualization,
|
||||
'7.16.0': moveDefaultReversedPaletteToCustom,
|
||||
'8.1.0': renameFilterReferences,
|
||||
};
|
||||
|
||||
export const mergeSavedObjectMigrationMaps = (
|
||||
obj1: SavedObjectMigrationMap,
|
||||
obj2: SavedObjectMigrationMap
|
||||
): SavedObjectMigrationMap => {
|
||||
const customizer = (objValue: SavedObjectMigrationFn, srcValue: SavedObjectMigrationFn) => {
|
||||
if (!srcValue || !objValue) {
|
||||
return srcValue || objValue;
|
||||
}
|
||||
return (state: SavedObjectUnsanitizedDoc, context: SavedObjectMigrationContext) =>
|
||||
objValue(srcValue(state, context), context);
|
||||
};
|
||||
|
||||
return mergeWith({ ...obj1 }, obj2, customizer);
|
||||
};
|
||||
|
||||
export const getAllMigrations = (
|
||||
filterMigrations: MigrateFunctionsObject
|
||||
): SavedObjectMigrationMap =>
|
||||
mergeSavedObjectMigrationMaps(lensMigrations, getLensFilterMigrations(filterMigrations));
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import type { PaletteOutput } from 'src/plugins/charts/common';
|
||||
import { Filter } from '@kbn/es-query';
|
||||
import { Query } from 'src/plugins/data/public';
|
||||
import type { CustomPaletteParams, LayerType } from '../../common';
|
||||
import type { CustomPaletteParams, LayerType, PersistableFilter } from '../../common';
|
||||
|
||||
export type OperationTypePre712 =
|
||||
| 'avg'
|
||||
|
@ -191,10 +191,17 @@ export interface LensDocShape715<VisualizationState = unknown> {
|
|||
};
|
||||
visualization: VisualizationState;
|
||||
query: Query;
|
||||
filters: Filter[];
|
||||
filters: PersistableFilter[];
|
||||
};
|
||||
}
|
||||
|
||||
export type LensDocShape810<VisualizationState = unknown> = Omit<
|
||||
LensDocShape715<VisualizationState>,
|
||||
'filters'
|
||||
> & {
|
||||
filters: Filter[];
|
||||
};
|
||||
|
||||
export type VisState716 =
|
||||
// Datatable
|
||||
| {
|
||||
|
|
|
@ -7,7 +7,10 @@
|
|||
|
||||
import { Plugin, CoreSetup, CoreStart, PluginInitializerContext, Logger } from 'src/core/server';
|
||||
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
|
||||
import { PluginStart as DataPluginStart } from 'src/plugins/data/server';
|
||||
import {
|
||||
PluginStart as DataPluginStart,
|
||||
PluginSetup as DataPluginSetup,
|
||||
} from 'src/plugins/data/server';
|
||||
import { ExpressionsServerSetup } from 'src/plugins/expressions/server';
|
||||
import { FieldFormatsStart } from 'src/plugins/field_formats/server';
|
||||
import { TaskManagerSetupContract, TaskManagerStartContract } from '../../task_manager/server';
|
||||
|
@ -19,14 +22,15 @@ import {
|
|||
} from './usage';
|
||||
import { setupSavedObjects } from './saved_objects';
|
||||
import { EmbeddableSetup } from '../../../../src/plugins/embeddable/server';
|
||||
import { lensEmbeddableFactory } from './embeddable/lens_embeddable_factory';
|
||||
import { setupExpressions } from './expressions';
|
||||
import { makeLensEmbeddableFactory } from './embeddable/make_lens_embeddable_factory';
|
||||
|
||||
export interface PluginSetupContract {
|
||||
usageCollection?: UsageCollectionSetup;
|
||||
taskManager?: TaskManagerSetupContract;
|
||||
embeddable: EmbeddableSetup;
|
||||
expressions: ExpressionsServerSetup;
|
||||
data: DataPluginSetup;
|
||||
}
|
||||
|
||||
export interface PluginStartContract {
|
||||
|
@ -36,7 +40,7 @@ export interface PluginStartContract {
|
|||
}
|
||||
|
||||
export interface LensServerPluginSetup {
|
||||
lensEmbeddableFactory: typeof lensEmbeddableFactory;
|
||||
lensEmbeddableFactory: ReturnType<typeof makeLensEmbeddableFactory>;
|
||||
}
|
||||
|
||||
export class LensServerPlugin implements Plugin<LensServerPluginSetup, {}, {}, {}> {
|
||||
|
@ -47,7 +51,8 @@ export class LensServerPlugin implements Plugin<LensServerPluginSetup, {}, {}, {
|
|||
}
|
||||
|
||||
setup(core: CoreSetup<PluginStartContract>, plugins: PluginSetupContract) {
|
||||
setupSavedObjects(core);
|
||||
const filterMigrations = plugins.data.query.filterManager.getAllMigrations();
|
||||
setupSavedObjects(core, filterMigrations);
|
||||
setupRoutes(core, this.initializerContext.logger.get());
|
||||
setupExpressions(core, plugins.expressions);
|
||||
|
||||
|
@ -61,6 +66,7 @@ export class LensServerPlugin implements Plugin<LensServerPluginSetup, {}, {}, {
|
|||
initializeLensTelemetry(this.telemetryLogger, core, plugins.taskManager);
|
||||
}
|
||||
|
||||
const lensEmbeddableFactory = makeLensEmbeddableFactory(filterMigrations);
|
||||
plugins.embeddable.registerEmbeddableFactory(lensEmbeddableFactory());
|
||||
return {
|
||||
lensEmbeddableFactory,
|
||||
|
|
|
@ -6,10 +6,11 @@
|
|||
*/
|
||||
|
||||
import { CoreSetup } from 'kibana/server';
|
||||
import { MigrateFunctionsObject } from '../../../../src/plugins/kibana_utils/common';
|
||||
import { getEditPath } from '../common';
|
||||
import { migrations } from './migrations/saved_object_migrations';
|
||||
import { getAllMigrations } from './migrations/saved_object_migrations';
|
||||
|
||||
export function setupSavedObjects(core: CoreSetup) {
|
||||
export function setupSavedObjects(core: CoreSetup, filterMigrations: MigrateFunctionsObject) {
|
||||
core.savedObjects.registerType({
|
||||
name: 'lens',
|
||||
hidden: false,
|
||||
|
@ -25,7 +26,7 @@ export function setupSavedObjects(core: CoreSetup) {
|
|||
uiCapabilitiesPath: 'visualize.show',
|
||||
}),
|
||||
},
|
||||
migrations,
|
||||
migrations: getAllMigrations(filterMigrations),
|
||||
mappings: {
|
||||
properties: {
|
||||
title: {
|
||||
|
|
|
@ -157,7 +157,7 @@ function getLensAttributes(
|
|||
{
|
||||
$state: { store: FilterStateStore.APP_STATE },
|
||||
meta: {
|
||||
indexRefName: 'filter-index-pattern-0',
|
||||
index: 'filter-index-pattern-0',
|
||||
negate: false,
|
||||
alias: null,
|
||||
disabled: false,
|
||||
|
@ -180,7 +180,7 @@ function getLensAttributes(
|
|||
meta: {
|
||||
alias: 'agent IDs',
|
||||
disabled: false,
|
||||
indexRefName: 'filter-index-pattern-0',
|
||||
index: 'filter-index-pattern-0',
|
||||
key: 'query',
|
||||
negate: false,
|
||||
type: 'custom',
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue