[Lens] Expose vis registration (#122348)

* expose vis registration

* add example app

* remove file

* fix and stabilize

* tsconfig fix

* fix type problems

* handle migrations

* fix problems

* fix tests

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Joe Reuter 2022-02-04 21:18:13 +01:00 committed by GitHub
parent d9d3230b94
commit 2f869baf18
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
36 changed files with 1023 additions and 99 deletions

View file

@ -13,11 +13,11 @@ import { GetMigrationFunctionObjectFn } from 'src/plugins/kibana_utils/common';
describe('embeddable migrations', () => {
test('should have all saved object migrations versions (>7.13.0)', () => {
const savedObjectMigrationVersions = Object.keys(getAllMigrations({})).filter((version) => {
const savedObjectMigrationVersions = Object.keys(getAllMigrations({}, {})).filter((version) => {
return semverGte(version, '7.13.1');
});
const embeddableMigrationVersions = (
makeLensEmbeddableFactory(() => ({}))()?.migrations as GetMigrationFunctionObjectFn
makeLensEmbeddableFactory(() => ({}), {})()?.migrations as GetMigrationFunctionObjectFn
)();
if (embeddableMigrationVersions) {
expect(savedObjectMigrationVersions.sort()).toEqual(
@ -47,14 +47,17 @@ describe('embeddable migrations', () => {
};
const migrations = (
makeLensEmbeddableFactory(() => ({
[migrationVersion]: (filters: Filter[]) => {
return filters.map((filterState) => ({
...filterState,
migrated: true,
}));
},
}))()?.migrations as GetMigrationFunctionObjectFn
makeLensEmbeddableFactory(
() => ({
[migrationVersion]: (filters: Filter[]) => {
return filters.map((filterState) => ({
...filterState,
migrated: true,
}));
},
}),
{}
)()?.migrations as GetMigrationFunctionObjectFn
)();
const migratedLensDoc = migrations[migrationVersion](lensVisualizationDoc);
@ -76,4 +79,57 @@ describe('embeddable migrations', () => {
},
});
});
test('should properly apply a custom visualization migration', () => {
const migrationVersion = 'some-version';
const lensVisualizationDoc = {
attributes: {
visualizationType: 'abc',
state: {
visualization: { oldState: true },
},
},
};
const migrationFn = jest.fn((oldState: { oldState: boolean }) => ({
newState: oldState.oldState,
}));
const embeddableMigrationVersions = (
makeLensEmbeddableFactory(() => ({}), {
abc: () => ({
[migrationVersion]: migrationFn,
}),
})()?.migrations as GetMigrationFunctionObjectFn
)();
const migratedLensDoc = embeddableMigrationVersions?.[migrationVersion](lensVisualizationDoc);
const otherLensDoc = embeddableMigrationVersions?.[migrationVersion]({
...lensVisualizationDoc,
attributes: {
...lensVisualizationDoc.attributes,
visualizationType: 'def',
},
});
expect(migrationFn).toHaveBeenCalledTimes(1);
expect(migratedLensDoc).toEqual({
attributes: {
visualizationType: 'abc',
state: {
visualization: { newState: true },
},
},
});
expect(otherLensDoc).toEqual({
attributes: {
visualizationType: 'def',
state: {
visualization: { oldState: true },
},
},
});
});
});

View file

@ -19,9 +19,11 @@ import {
commonRenameOperationsForFormula,
commonRenameRecordsField,
commonUpdateVisLayerType,
getLensCustomVisualizationMigrations,
getLensFilterMigrations,
} from '../migrations/common_migrations';
import {
CustomVisualizationMigrations,
LensDocShape713,
LensDocShape715,
LensDocShapePre712,
@ -31,55 +33,64 @@ import {
import { extract, inject } from '../../common/embeddable_factory';
export const makeLensEmbeddableFactory =
(getFilterMigrations: () => MigrateFunctionsObject) => (): EmbeddableRegistryDefinition => {
(
getFilterMigrations: () => MigrateFunctionsObject,
customVisualizationMigrations: CustomVisualizationMigrations
) =>
(): EmbeddableRegistryDefinition => {
return {
id: DOC_TYPE,
migrations: () =>
mergeMigrationFunctionMaps(getLensFilterMigrations(getFilterMigrations()), {
// 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 };
const migratedLensState = commonRenameRecordsField(
commonRenameFilterReferences(lensState.attributes)
);
return {
...lensState,
attributes: migratedLensState,
} as unknown as SerializableRecord;
},
}),
mergeMigrationFunctionMaps(
mergeMigrationFunctionMaps(getLensFilterMigrations(getFilterMigrations()), {
// 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 };
const migratedLensState = commonRenameRecordsField(
commonRenameFilterReferences(lensState.attributes)
);
return {
...lensState,
attributes: migratedLensState,
} as unknown as SerializableRecord;
},
}),
getLensCustomVisualizationMigrations(customVisualizationMigrations)
),
extract,
inject,
};

View file

@ -7,7 +7,12 @@
import { cloneDeep, mapValues } from 'lodash';
import { PaletteOutput } from 'src/plugins/charts/common';
import { MigrateFunctionsObject } from '../../../../../src/plugins/kibana_utils/common';
import { SerializableRecord } from '@kbn/utility-types';
import {
mergeMigrationFunctionMaps,
MigrateFunction,
MigrateFunctionsObject,
} from '../../../../../src/plugins/kibana_utils/common';
import {
LensDocShapePre712,
OperationTypePre712,
@ -18,6 +23,7 @@ import {
VisStatePost715,
VisStatePre715,
VisState716,
CustomVisualizationMigrations,
LensDocShape810,
} from './types';
import { CustomPaletteParams, DOCUMENT_FIELD_NAME, layerTypes } from '../../common';
@ -186,6 +192,51 @@ export const commonRenameFilterReferences = (attributes: LensDocShape715): LensD
return newAttributes as LensDocShape810;
};
const getApplyCustomVisualizationMigrationToLens = (id: string, migration: MigrateFunction) => {
return (savedObject: { attributes: LensDocShape }) => {
if (savedObject.attributes.visualizationType !== id) return savedObject;
return {
...savedObject,
attributes: {
...savedObject.attributes,
state: {
...savedObject.attributes.state,
visualization: migration(
savedObject.attributes.state.visualization as SerializableRecord
),
},
},
};
};
};
/**
* This creates a migration map that applies custom visualization migrations
*/
export const getLensCustomVisualizationMigrations = (
customVisualizationMigrations: CustomVisualizationMigrations
) => {
return Object.entries(customVisualizationMigrations)
.map(([id, migrationGetter]) => {
const migrationMap: MigrateFunctionsObject = {};
const currentMigrations = migrationGetter();
for (const version in currentMigrations) {
if (currentMigrations.hasOwnProperty(version)) {
migrationMap[version] = getApplyCustomVisualizationMigrationToLens(
id,
currentMigrations[version]
);
}
}
return migrationMap;
})
.reduce(
(fullMigrationMap, currentVisualizationTypeMigrationMap) =>
mergeMigrationFunctionMaps(fullMigrationMap, currentVisualizationTypeMigrationMap),
{}
);
};
/**
* This creates a migration map that applies filter migrations to Lens visualizations
*/

View file

@ -24,7 +24,7 @@ import { PaletteOutput } from 'src/plugins/charts/common';
import { Filter } from '@kbn/es-query';
describe('Lens migrations', () => {
const migrations = getAllMigrations({});
const migrations = getAllMigrations({}, {});
describe('7.7.0 missing dimensions in XY', () => {
const context = {} as SavedObjectMigrationContext;
@ -1611,14 +1611,17 @@ describe('Lens migrations', () => {
},
};
const migrationFunctionsObject = getAllMigrations({
[migrationVersion]: (filters: Filter[]) => {
return filters.map((filterState) => ({
...filterState,
migrated: true,
}));
const migrationFunctionsObject = getAllMigrations(
{
[migrationVersion]: (filters: Filter[]) => {
return filters.map((filterState) => ({
...filterState,
migrated: true,
}));
},
},
});
{}
);
const migratedLensDoc = migrationFunctionsObject[migrationVersion](
lensVisualizationDoc as SavedObjectUnsanitizedDoc,
@ -1642,4 +1645,63 @@ describe('Lens migrations', () => {
},
});
});
test('should properly apply a custom visualization migration', () => {
const migrationVersion = 'some-version';
const lensVisualizationDoc = {
attributes: {
visualizationType: 'abc',
state: {
visualization: { oldState: true },
},
},
};
const migrationFn = jest.fn((oldState: { oldState: boolean }) => ({
newState: oldState.oldState,
}));
const migrationFunctionsObject = getAllMigrations(
{},
{
abc: () => ({
[migrationVersion]: migrationFn,
}),
}
);
const migratedLensDoc = migrationFunctionsObject[migrationVersion](
lensVisualizationDoc as SavedObjectUnsanitizedDoc,
{} as SavedObjectMigrationContext
);
const otherLensDoc = migrationFunctionsObject[migrationVersion](
{
...lensVisualizationDoc,
attributes: {
...lensVisualizationDoc.attributes,
visualizationType: 'def',
},
} as SavedObjectUnsanitizedDoc,
{} as SavedObjectMigrationContext
);
expect(migrationFn).toHaveBeenCalledTimes(1);
expect(migratedLensDoc).toEqual({
attributes: {
visualizationType: 'abc',
state: {
visualization: { newState: true },
},
},
});
expect(otherLensDoc).toEqual({
attributes: {
visualizationType: 'def',
state: {
visualization: { oldState: true },
},
},
});
});
});

View file

@ -27,6 +27,7 @@ import {
VisStatePost715,
VisStatePre715,
VisState716,
CustomVisualizationMigrations,
LensDocShape810,
} from './types';
import {
@ -36,6 +37,7 @@ import {
commonMakeReversePaletteAsCustom,
commonRenameFilterReferences,
getLensFilterMigrations,
getLensCustomVisualizationMigrations,
commonRenameRecordsField,
} from './common_migrations';
@ -473,9 +475,13 @@ const lensMigrations: SavedObjectMigrationMap = {
};
export const getAllMigrations = (
filterMigrations: MigrateFunctionsObject
filterMigrations: MigrateFunctionsObject,
customVisualizationMigrations: CustomVisualizationMigrations
): SavedObjectMigrationMap =>
mergeSavedObjectMigrationMaps(
lensMigrations,
getLensFilterMigrations(filterMigrations) as unknown as SavedObjectMigrationMap
mergeSavedObjectMigrationMaps(
lensMigrations,
getLensFilterMigrations(filterMigrations) as unknown as SavedObjectMigrationMap
),
getLensCustomVisualizationMigrations(customVisualizationMigrations)
);

View file

@ -8,8 +8,11 @@
import type { PaletteOutput } from 'src/plugins/charts/common';
import { Filter } from '@kbn/es-query';
import { Query } from 'src/plugins/data/public';
import type { MigrateFunctionsObject } from 'src/plugins/kibana_utils/common';
import type { CustomPaletteParams, LayerType, PersistableFilter } from '../../common';
export type CustomVisualizationMigrations = Record<string, () => MigrateFunctionsObject>;
export type OperationTypePre712 =
| 'avg'
| 'cardinality'

View file

@ -14,6 +14,8 @@ import {
} from 'src/plugins/data/server';
import { ExpressionsServerSetup } from 'src/plugins/expressions/server';
import { FieldFormatsStart } from 'src/plugins/field_formats/server';
import type { MigrateFunctionsObject } from 'src/plugins/kibana_utils/common';
import { TaskManagerSetupContract, TaskManagerStartContract } from '../../task_manager/server';
import { setupRoutes } from './routes';
import { getUiSettings } from './ui_settings';
@ -26,6 +28,7 @@ import { setupSavedObjects } from './saved_objects';
import { EmbeddableSetup } from '../../../../src/plugins/embeddable/server';
import { setupExpressions } from './expressions';
import { makeLensEmbeddableFactory } from './embeddable/make_lens_embeddable_factory';
import type { CustomVisualizationMigrations } from './migrations/types';
export interface PluginSetupContract {
usageCollection?: UsageCollectionSetup;
@ -43,11 +46,22 @@ export interface PluginStartContract {
}
export interface LensServerPluginSetup {
/**
* Server side embeddable definition which provides migrations to run if Lens state is embedded into another saved object somewhere
*/
lensEmbeddableFactory: ReturnType<typeof makeLensEmbeddableFactory>;
/**
* Register custom migration functions for custom third party Lens visualizations
*/
registerVisualizationMigration: (
id: string,
migrationsGetter: () => MigrateFunctionsObject
) => void;
}
export class LensServerPlugin implements Plugin<LensServerPluginSetup, {}, {}, {}> {
private readonly telemetryLogger: Logger;
private customVisualizationMigrations: CustomVisualizationMigrations = {};
constructor(private initializerContext: PluginInitializerContext) {
this.telemetryLogger = initializerContext.logger.get('usage');
@ -57,7 +71,7 @@ export class LensServerPlugin implements Plugin<LensServerPluginSetup, {}, {}, {
const getFilterMigrations = plugins.data.query.filterManager.getAllMigrations.bind(
plugins.data.query.filterManager
);
setupSavedObjects(core, getFilterMigrations);
setupSavedObjects(core, getFilterMigrations, this.customVisualizationMigrations);
setupRoutes(core, this.initializerContext.logger.get());
setupExpressions(core, plugins.expressions);
core.uiSettings.register(getUiSettings());
@ -72,10 +86,22 @@ export class LensServerPlugin implements Plugin<LensServerPluginSetup, {}, {}, {
initializeLensTelemetry(this.telemetryLogger, core, plugins.taskManager);
}
const lensEmbeddableFactory = makeLensEmbeddableFactory(getFilterMigrations);
const lensEmbeddableFactory = makeLensEmbeddableFactory(
getFilterMigrations,
this.customVisualizationMigrations
);
plugins.embeddable.registerEmbeddableFactory(lensEmbeddableFactory());
return {
lensEmbeddableFactory,
registerVisualizationMigration: (
id: string,
migrationsGetter: () => MigrateFunctionsObject
) => {
if (this.customVisualizationMigrations[id]) {
throw new Error(`Migrations object for visualization ${id} registered already`);
}
this.customVisualizationMigrations[id] = migrationsGetter;
},
};
}

View file

@ -9,10 +9,12 @@ import { CoreSetup } from 'kibana/server';
import { MigrateFunctionsObject } from '../../../../src/plugins/kibana_utils/common';
import { getEditPath } from '../common';
import { getAllMigrations } from './migrations/saved_object_migrations';
import { CustomVisualizationMigrations } from './migrations/types';
export function setupSavedObjects(
core: CoreSetup,
getFilterMigrations: () => MigrateFunctionsObject
getFilterMigrations: () => MigrateFunctionsObject,
customVisualizationMigrations: CustomVisualizationMigrations
) {
core.savedObjects.registerType({
name: 'lens',
@ -29,7 +31,7 @@ export function setupSavedObjects(
uiCapabilitiesPath: 'visualize.show',
}),
},
migrations: () => getAllMigrations(getFilterMigrations()),
migrations: () => getAllMigrations(getFilterMigrations(), customVisualizationMigrations),
mappings: {
properties: {
title: {