[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

@ -0,0 +1,5 @@
{
"rules": {
"@typescript-eslint/consistent-type-definitions": 0
}
}

View file

@ -0,0 +1,23 @@
# Third party Lens visualization
To run this example plugin, use the command `yarn start --run-examples`.
This example shows how to register a visualization to Lens which lives along the regular visualizations (xy, table and so on).
The following parts can be seen in this example:
* Registering the visualization type so it shows up in the Lens editor along with custom edit UI and hooks to update state on user interactions (add dimension, delete dimension).
* Registering the used expression functions and expression renderers to actually render the expression into a DOM element.
* Providing a sample migration on the Kibana server which allows to update existing stored visualizations and change their state on Kibana upgrade / import of old saved objects.
To test the migration, you can import the following ndjson file via saved object import (requires installed logs sample data):
<details>
<summary>Click to expand</summary>
```
{"attributes":{"fieldFormatMap":"{\"hour_of_day\":{}}","runtimeFieldMap":"{\"hour_of_day\":{\"type\":\"long\",\"script\":{\"source\":\"emit(doc['timestamp'].value.getHour());\"}}}","timeFieldName":"timestamp","title":"kibana_sample_data_logs"},"coreMigrationVersion":"8.0.0","id":"90943e30-9a47-11e8-b64d-95841ca0b247","migrationVersion":{"index-pattern":"8.0.0"},"references":[],"type":"index-pattern","updated_at":"2022-01-24T10:54:24.209Z","version":"WzQzMTQ3LDFd"}
{"attributes":{"description":"","state":{"datasourceStates":{"indexpattern":{"layers":{"f2700077-50bf-48e4-829c-f695f87e226d":{"columnOrder":["5e704cac-8490-457a-b635-01f3a5a132b7"],"columns":{"5e704cac-8490-457a-b635-01f3a5a132b7":{"dataType":"number","isBucketed":false,"label":"Count of records","operationType":"count","scale":"ratio","sourceField":"Records"}},"incompleteColumns":{}}}}},"filters":[],"query":{"language":"kuery","query":""},"visualization":{"column":"5e704cac-8490-457a-b635-01f3a5a132b7","layerId":"f2700077-50bf-48e4-829c-f695f87e226d"}},"title":"Rotating number test","visualizationType":"rotatingNumber"},"coreMigrationVersion":"8.0.0","id":"468f0be0-7e86-11ec-9739-d570ffd3fbe4","migrationVersion":{"lens":"8.0.0"},"references":[{"id":"90943e30-9a47-11e8-b64d-95841ca0b247","name":"indexpattern-datasource-current-indexpattern","type":"index-pattern"},{"id":"90943e30-9a47-11e8-b64d-95841ca0b247","name":"indexpattern-datasource-layer-f2700077-50bf-48e4-829c-f695f87e226d","type":"index-pattern"}],"type":"lens","updated_at":"2022-01-26T08:59:31.618Z","version":"WzQzNjUzLDFd"}
{"excludedObjects":[],"excludedObjectsCount":0,"exportedCount":2,"missingRefCount":0,"missingReferences":[]}
```
</details>

View file

@ -0,0 +1,8 @@
/*
* 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.
*/
export const DEFAULT_COLOR = '#000000';

View file

@ -0,0 +1,12 @@
/*
* 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.
*/
export interface RotatingNumberState {
accessor?: string;
color: string;
layerId: string;
}

View file

@ -0,0 +1,24 @@
{
"id": "thirdPartyVisLensExample",
"version": "0.0.1",
"kibanaVersion": "kibana",
"configPath": ["third_part_vis_lens_example"],
"server": true,
"ui": true,
"requiredPlugins": [
"lens",
"dataViews",
"embeddable",
"developerExamples",
"expressions",
"fieldFormats"
],
"optionalPlugins": [],
"requiredBundles": [
"kibanaReact"
],
"owner": {
"name": "Vis Editors",
"githubTeam": "kibana-vis-editors"
}
}

View file

@ -0,0 +1,14 @@
{
"name": "third_party_vis_lens_example",
"version": "1.0.0",
"main": "target/examples/third_party_vis_lens_example",
"kibana": {
"version": "kibana",
"templateVersion": "1.0.0"
},
"license": "Elastic License 2.0",
"scripts": {
"kbn": "node ../../../scripts/kbn.js",
"build": "rm -rf './target' && ../../../node_modules/.bin/tsc"
}
}

View file

@ -0,0 +1,133 @@
/*
* 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 React from 'react';
import ReactDOM from 'react-dom';
import { css, keyframes } from '@emotion/css';
import type {
Datatable,
ExpressionFunctionDefinition,
ExpressionRenderDefinition,
IInterpreterRenderHandlers,
} from '../../../../src/plugins/expressions/public';
import { RotatingNumberState } from '../common/types';
import { FormatFactory } from '../../../../src/plugins/field_formats/common';
export const getRotatingNumberRenderer = (
formatFactory: Promise<FormatFactory>
): ExpressionRenderDefinition<RotatingNumberChartProps> => ({
name: 'rotating_number',
displayName: 'Rotating number',
help: 'Rotating number renderer',
validate: () => undefined,
reuseDomNode: true,
render: async (
domNode: Element,
config: RotatingNumberChartProps,
handlers: IInterpreterRenderHandlers
) => {
ReactDOM.render(
<RotatingNumberChart {...config} formatFactory={await formatFactory} />,
domNode,
() => {
handlers.done();
}
);
handlers.onDestroy(() => ReactDOM.unmountComponentAtNode(domNode));
},
});
const rotating = keyframes`
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
`;
function RotatingNumberChart({
data,
args,
formatFactory,
}: RotatingNumberChartProps & { formatFactory: FormatFactory }) {
const { accessor, color } = args;
const column = data.columns.find((col) => col.id === accessor);
const rawValue = accessor && data.rows[0]?.[accessor];
const value =
column && column.meta?.params
? formatFactory(column.meta?.params).convert(rawValue)
: Number(Number(rawValue).toFixed(3)).toString();
return (
<div
className={css`
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
text-align: center;
font-size: 100px;
overflow: hidden;
`}
>
<div
className={css`
color: ${color};
animation: ${rotating} 5s linear infinite;
`}
>
{value}
</div>
</div>
);
}
export interface RotatingNumberChartProps {
data: Datatable;
args: RotatingNumberState;
}
interface RotatingNumberRender {
type: 'render';
as: 'rotating_number';
value: RotatingNumberChartProps;
}
export const rotatingNumberFunction: ExpressionFunctionDefinition<
'rotating_number',
Datatable,
Omit<RotatingNumberState, 'layerId' | 'layerType'>,
RotatingNumberRender
> = {
name: 'rotating_number',
type: 'render',
help: 'A rotating number',
args: {
accessor: {
types: ['string'],
help: 'The column whose value is being displayed',
},
color: {
types: ['string'],
help: 'Color of the number',
},
},
inputTypes: ['datatable'],
fn(data, args) {
return {
type: 'render',
as: 'rotating_number',
value: {
data,
args,
},
} as RotatingNumberRender;
},
};

View file

@ -0,0 +1,10 @@
/*
* 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 { EmbeddedLensExamplePlugin } from './plugin';
export const plugin = () => new EmbeddedLensExamplePlugin();

View file

@ -0,0 +1,138 @@
/*
* 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 { ExpressionsSetup } from 'src/plugins/expressions/public';
import { FieldFormatsStart } from 'src/plugins/field_formats/public';
import { Plugin, CoreSetup, AppNavLinkStatus } from '../../../../src/core/public';
import { DataViewsPublicPluginStart, DataView } from '../../../../src/plugins/data_views/public';
import { LensPublicSetup, LensPublicStart } from '../../../plugins/lens/public';
import { DeveloperExamplesSetup } from '../../../../examples/developer_examples/public';
import { TypedLensByValueInput, PersistedIndexPatternLayer } from '../../../plugins/lens/public';
import { getRotatingNumberRenderer, rotatingNumberFunction } from './expression';
import { getRotatingNumberVisualization } from './visualization';
import { RotatingNumberState } from '../common/types';
export interface SetupDependencies {
developerExamples: DeveloperExamplesSetup;
lens: LensPublicSetup;
expressions: ExpressionsSetup;
}
export interface StartDependencies {
dataViews: DataViewsPublicPluginStart;
lens: LensPublicStart;
fieldFormats: FieldFormatsStart;
}
function getLensAttributes(defaultDataView: DataView): TypedLensByValueInput['attributes'] {
const dataLayer: PersistedIndexPatternLayer = {
columnOrder: ['col1'],
columns: {
col1: {
dataType: 'number',
isBucketed: false,
label: 'Count of records',
operationType: 'count',
scale: 'ratio',
sourceField: 'Records',
},
},
};
const rotatingNumberConfig: RotatingNumberState = {
accessor: 'col1',
color: '#ff0000',
layerId: 'layer1',
};
return {
visualizationType: 'rotatingNumber',
title: 'Prefilled from example app',
references: [
{
id: defaultDataView.id!,
name: 'indexpattern-datasource-current-indexpattern',
type: 'index-pattern',
},
{
id: defaultDataView.id!,
name: 'indexpattern-datasource-layer-layer1',
type: 'index-pattern',
},
],
state: {
datasourceStates: {
indexpattern: {
layers: {
layer1: dataLayer,
},
},
},
filters: [],
query: { language: 'kuery', query: '' },
visualization: rotatingNumberConfig,
},
};
}
export class EmbeddedLensExamplePlugin
implements Plugin<void, void, SetupDependencies, StartDependencies>
{
public setup(
core: CoreSetup<StartDependencies>,
{ developerExamples, lens, expressions }: SetupDependencies
) {
core.application.register({
id: 'third_party_lens_vis_example',
title: 'Third party Lens vis example',
navLinkStatus: AppNavLinkStatus.hidden,
mount: (params) => {
(async () => {
const [, { lens: lensStart, dataViews }] = await core.getStartServices();
const defaultDataView = await dataViews.getDefault();
lensStart.navigateToPrefilledEditor({
id: '',
timeRange: {
from: 'now-5d',
to: 'now',
},
attributes: getLensAttributes(defaultDataView!),
});
})();
return () => {};
},
});
developerExamples.register({
appId: 'third_party_lens_vis_example',
title: 'Third party Lens visualization',
description: 'Add custom visualization types to the Lens editor',
links: [
{
label: 'README',
href: 'https://github.com/elastic/kibana/tree/main/x-pack/examples/third_party_lens_vis_example',
iconType: 'logoGithub',
size: 's',
target: '_blank',
},
],
});
expressions.registerRenderer(() =>
getRotatingNumberRenderer(
core.getStartServices().then(([, { fieldFormats }]) => fieldFormats.deserialize)
)
);
expressions.registerFunction(() => rotatingNumberFunction);
lens.registerVisualization(async () => getRotatingNumberVisualization({ theme: core.theme }));
}
public start() {}
public stop() {}
}

View file

@ -0,0 +1,183 @@
/*
* 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 React from 'react';
import { EuiFormRow, EuiColorPicker } from '@elastic/eui';
import { render } from 'react-dom';
import { Ast } from '@kbn/interpreter';
import { ThemeServiceStart } from '../../../../src/core/public';
import { KibanaThemeProvider } from '../../../../src/plugins/kibana_react/public';
import { Visualization, OperationMetadata } from '../../../plugins/lens/public';
import type { RotatingNumberState } from '../common/types';
import { DEFAULT_COLOR } from '../common/constants';
import { layerTypes } from '../../../plugins/lens/public';
const toExpression = (state: RotatingNumberState): Ast | null => {
if (!state.accessor) {
return null;
}
return {
type: 'expression',
chain: [
{
type: 'function',
function: 'rotating_number',
arguments: {
accessor: [state.accessor],
color: [state?.color || 'black'],
},
},
],
};
};
export const getRotatingNumberVisualization = ({
theme,
}: {
theme: ThemeServiceStart;
}): Visualization<RotatingNumberState> => ({
id: 'rotatingNumber',
visualizationTypes: [
{
id: 'rotatingNumber',
icon: 'refresh',
label: 'Rotating number',
groupLabel: 'Goal and single value',
sortPriority: 3,
},
],
getVisualizationTypeId() {
return 'rotatingNumber';
},
clearLayer(state) {
return {
...state,
accessor: undefined,
};
},
getLayerIds(state) {
return [state.layerId];
},
getDescription() {
return {
icon: 'refresh',
label: 'A number that rotates',
};
},
getSuggestions: ({ state, table }) => {
if (table.columns.length > 1) {
return [];
}
if (state && table.changeType === 'unchanged') {
return [];
}
const column = table.columns[0];
if (column.operation.isBucketed || column.operation.dataType !== 'number') {
return [];
}
return [
{
previewIcon: 'refresh',
score: 0.5,
title: `Rotating ${table.label}` || 'Rotating number',
state: {
layerId: table.layerId,
color: state?.color || DEFAULT_COLOR,
accessor: column.columnId,
},
},
];
},
initialize(addNewLayer, state) {
return (
state || {
layerId: addNewLayer(),
accessor: undefined,
color: DEFAULT_COLOR,
}
);
},
getConfiguration(props) {
return {
groups: [
{
groupId: 'metric',
groupLabel: 'Rotating number',
layerId: props.state.layerId,
accessors: props.state.accessor
? [
{
columnId: props.state.accessor,
triggerIcon: 'color',
color: props.state.color,
},
]
: [],
supportsMoreColumns: !props.state.accessor,
filterOperations: (op: OperationMetadata) => !op.isBucketed && op.dataType === 'number',
enableDimensionEditor: true,
required: true,
},
],
};
},
getSupportedLayers() {
return [
{
type: layerTypes.DATA,
label: 'Add visualization layer',
},
];
},
getLayerType(layerId, state) {
if (state?.layerId === layerId) {
return layerTypes.DATA;
}
},
toExpression: (state) => toExpression(state),
toPreviewExpression: (state) => toExpression(state),
setDimension({ prevState, columnId }) {
return { ...prevState, accessor: columnId };
},
removeDimension({ prevState }) {
return { ...prevState, accessor: undefined };
},
renderDimensionEditor(domElement, props) {
render(
<KibanaThemeProvider theme$={theme.theme$}>
<EuiFormRow label="Pick a color">
<EuiColorPicker
onChange={(newColor) => {
props.setState({ ...props.state, color: newColor });
}}
color={props.state.color}
/>
</EuiFormRow>
</KibanaThemeProvider>,
domElement
);
},
getErrorMessages(state) {
// Is it possible to break it?
return undefined;
},
});

View file

@ -0,0 +1,11 @@
/*
* 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 { PluginInitializer } from 'kibana/server';
import { ThirdPartyVisLensExamplePlugin } from './plugin';
export const plugin: PluginInitializer<void, void> = () => new ThirdPartyVisLensExamplePlugin();

View file

@ -0,0 +1,42 @@
/*
* 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 { Plugin, CoreSetup } from 'kibana/server';
import { LensServerPluginSetup } from '../../../plugins/lens/server';
import { DEFAULT_COLOR } from '../common/constants';
import { RotatingNumberState as Post81RotatingNumberState } from '../common/types';
// Old versions of this visualization had a slightly different shape of state
interface Pre81RotatingNumberState {
column?: string;
layerId: string;
}
// this plugin's dependencies
export interface Dependencies {
lens: LensServerPluginSetup;
}
export class ThirdPartyVisLensExamplePlugin implements Plugin<void, void, Dependencies> {
public setup(core: CoreSetup, { lens }: Dependencies) {
lens.registerVisualizationMigration('rotatingNumber', () => ({
// Example state migration which will be picked by all the places Lens visualizations are stored
'8.1.0': (oldState: Pre81RotatingNumberState): Post81RotatingNumberState => {
return {
// column gets renamed to accessor
accessor: oldState.column,
// layer id just gets copied over
layerId: oldState.layerId,
// color gets pre-set with default color
color: DEFAULT_COLOR,
};
},
}));
}
public start() {}
public stop() {}
}

View file

@ -0,0 +1,24 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./target/types"
},
"include": [
"index.ts",
"public/**/*",
"server/**/*",
"common/**/*",
"../../../typings/**/*"
],
"exclude": [],
"references": [
{ "path": "../../../src/core/tsconfig.json" },
{ "path": "../../../src/plugins/expressions/tsconfig.json" },
{ "path": "../../../src/plugins/kibana_react/tsconfig.json" },
{ "path": "../../../src/plugins/data_views/tsconfig.json" },
{ "path": "../../../src/plugins/field_formats/tsconfig.json" },
{ "path": "../../../src/plugins/embeddable/tsconfig.json" },
{ "path": "../../plugins/lens/tsconfig.json" },
{ "path": "../../../examples/developer_examples/tsconfig.json" },
]
}

View file

@ -823,7 +823,7 @@ describe('common utils', () => {
].join('\n\n');
const extractedReferences = extractLensReferencesFromCommentString(
makeLensEmbeddableFactory(() => ({})),
makeLensEmbeddableFactory(() => ({}), {}),
commentString
);
@ -922,7 +922,7 @@ describe('common utils', () => {
].join('\n\n');
const updatedReferences = getOrUpdateLensReferences(
makeLensEmbeddableFactory(() => ({})),
makeLensEmbeddableFactory(() => ({}), {}),
newCommentString,
{
references: currentCommentReferences,

View file

@ -36,7 +36,7 @@ import { GENERATED_ALERT, SUB_CASE_SAVED_OBJECT } from './constants';
describe('comments migrations', () => {
const migrations = createCommentsMigrations({
lensEmbeddableFactory: makeLensEmbeddableFactory(() => ({})),
lensEmbeddableFactory: makeLensEmbeddableFactory(() => ({}), {}),
});
const contextMock = savedObjectsServiceMock.createMigrationContext();

View file

@ -24,6 +24,7 @@ import { TableDimensionEditor } from './components/dimension_editor';
import { CUSTOM_PALETTE } from '../shared_components/coloring/constants';
import { LayerType, layerTypes } from '../../common';
import { getDefaultSummaryLabel, PagingState } from '../../common/expressions';
import { VIS_EVENT_TO_TRIGGER } from '../../../../../src/plugins/visualizations/public';
import type { ColumnState, SortingState } from '../../common/expressions';
import { DataTableToolbar } from './components/toolbar';
export interface DatatableVisualizationState {
@ -84,6 +85,8 @@ export const getDatatableVisualization = ({
switchVisualizationType: (_, state) => state,
triggers: [VIS_EVENT_TO_TRIGGER.filter, VIS_EVENT_TO_TRIGGER.tableRowContextMenuClick],
initialize(addNewLayer, state) {
return (
state || {

View file

@ -47,6 +47,9 @@ export function EditorFrame(props: EditorFrameProps) {
const visualization = useLensSelector(selectVisualization);
const areDatasourcesLoaded = useLensSelector(selectAreDatasourcesLoaded);
const isVisualizationLoaded = !!visualization.state;
const visualizationTypeIsKnown = Boolean(
visualization.activeId && props.visualizationMap[visualization.activeId]
);
const framePublicAPI: FramePublicAPI = useLensSelector((state) =>
selectFramePublicAPI(state, datasourceMap)
);
@ -121,6 +124,7 @@ export function EditorFrame(props: EditorFrameProps) {
)
}
suggestionsPanel={
visualizationTypeIsKnown &&
areDatasourcesLoaded && (
<SuggestionPanelWrapper
ExpressionRenderer={props.ExpressionRenderer}

View file

@ -27,6 +27,7 @@ import {
getMissingCurrentDatasource,
getMissingIndexPatterns,
getMissingVisualizationTypeError,
getUnknownVisualizationTypeError,
} from '../error_helper';
import { DatasourceStates } from '../../state_management';
@ -97,6 +98,12 @@ export async function persistedStateToExpression(
errors: [{ shortMessage: '', longMessage: getMissingVisualizationTypeError() }],
};
}
if (!visualizations[visualizationType]) {
return {
ast: null,
errors: [getUnknownVisualizationTypeError(visualizationType)],
};
}
const visualization = visualizations[visualizationType!];
const datasourceStates = await initializeDatasources(
datasourceMap,

View file

@ -247,10 +247,13 @@ export const ChartSwitch = memo(function ChartSwitch(props: Props) {
if (!flyoutOpen) {
return { visualizationTypes: [], visualizationsLookup: {} };
}
const subVisualizationId = getCurrentVisualizationId(
props.visualizationMap[visualization.activeId || ''],
visualization.state
);
const subVisualizationId =
visualization.activeId && props.visualizationMap[visualization.activeId]
? getCurrentVisualizationId(
props.visualizationMap[visualization.activeId],
visualization.state
)
: undefined;
const lowercasedSearchTerm = searchTerm.toLowerCase();
// reorganize visualizations in groups
const grouped: Record<

View file

@ -46,7 +46,10 @@ import { UiActionsStart } from '../../../../../../../src/plugins/ui_actions/publ
import { VIS_EVENT_TO_TRIGGER } from '../../../../../../../src/plugins/visualizations/public';
import { WorkspacePanelWrapper } from './workspace_panel_wrapper';
import { DropIllustration } from '../../../assets/drop_illustration';
import { getOriginalRequestErrorMessages } from '../../error_helper';
import {
getOriginalRequestErrorMessages,
getUnknownVisualizationTypeError,
} from '../../error_helper';
import { getMissingIndexPattern, validateDatasourceAndVisualization } from '../state_helpers';
import { DefaultInspectorAdapters } from '../../../../../../../src/plugins/expressions/common';
import {
@ -163,6 +166,8 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({
]
: [];
const unknownVisError = visualization.activeId && !activeVisualization;
// Note: mind to all these eslint disable lines: the frameAPI will change too frequently
// and to prevent race conditions it is ok to leave them there.
@ -180,7 +185,7 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({
);
const expression = useMemo(() => {
if (!configurationValidationError?.length && !missingRefsErrors.length) {
if (!configurationValidationError?.length && !missingRefsErrors.length && !unknownVisError) {
try {
const ast = buildExpression({
visualization: activeVisualization,
@ -213,6 +218,12 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({
}));
}
}
if (unknownVisError) {
setLocalState((s) => ({
...s,
expressionBuildError: [getUnknownVisualizationTypeError(visualization.activeId!)],
}));
}
}, [
activeVisualization,
visualization.state,
@ -221,6 +232,8 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({
datasourceLayers,
configurationValidationError?.length,
missingRefsErrors.length,
unknownVisError,
visualization.activeId,
]);
const expressionExists = Boolean(expression);
@ -415,6 +428,7 @@ export const VisualizationWrapper = ({
fixAction?: DatasourceFixAction<unknown>;
}>;
missingRefsErrors?: Array<{ shortMessage: string; longMessage: React.ReactNode }>;
unknownVisError?: Array<{ shortMessage: string; longMessage: React.ReactNode }>;
};
ExpressionRendererComponent: ReactExpressionRendererType;
application: ApplicationStart;

View file

@ -165,3 +165,17 @@ export function getMissingIndexPatterns(indexPatternIds: string[]) {
values: { count: indexPatternIds.length, ids: indexPatternIds.join(', ') },
});
}
export function getUnknownVisualizationTypeError(visType: string) {
return {
shortMessage: i18n.translate('xpack.lens.unknownVisType.shortMessage', {
defaultMessage: `Unknown visualization type`,
}),
longMessage: i18n.translate('xpack.lens.unknownVisType.longMessage', {
defaultMessage: `The visualization type {visType} could not be resolved.`,
values: {
visType,
},
}),
};
}

View file

@ -265,22 +265,10 @@ export class Embeddable
}
public supportedTriggers() {
if (!this.savedVis) {
if (!this.savedVis || !this.savedVis.visualizationType) {
return [];
}
switch (this.savedVis.visualizationType) {
case 'lnsXY':
case 'lnsHeatmap':
return [VIS_EVENT_TO_TRIGGER.filter, VIS_EVENT_TO_TRIGGER.brush];
case 'lnsDatatable':
return [VIS_EVENT_TO_TRIGGER.filter, VIS_EVENT_TO_TRIGGER.tableRowContextMenuClick];
case 'lnsPie':
return [VIS_EVENT_TO_TRIGGER.filter];
case 'lnsGauge':
case 'lnsMetric':
default:
return [];
}
return this.deps.visualizationMap[this.savedVis.visualizationType]?.triggers || [];
}
public getInspectorAdapters() {

View file

@ -52,7 +52,8 @@ export type TypedLensByValueInput = Omit<LensByValueInput, 'attributes'> & {
| LensAttributes<'lnsDatatable', DatatableVisualizationState>
| LensAttributes<'lnsMetric', MetricState>
| LensAttributes<'lnsHeatmap', HeatmapVisualizationState>
| LensAttributes<'lnsGauge', GaugeVisualizationState>;
| LensAttributes<'lnsGauge', GaugeVisualizationState>
| LensAttributes<string, unknown>;
};
export type EmbeddableComponentProps = (TypedLensByValueInput | LensByReferenceInput) & {

View file

@ -12,7 +12,7 @@ export type {
TypedLensByValueInput,
} from './embeddable/embeddable_component';
export type { XYState } from './xy_visualization/types';
export type { DataType, OperationMetadata } from './types';
export type { DataType, OperationMetadata, Visualization } from './types';
export type {
PieVisualizationState,
PieLayerState,
@ -61,7 +61,8 @@ export type {
StaticValueIndexPatternColumn,
} from './indexpattern_datasource/types';
export type { LensEmbeddableInput } from './embeddable';
export { layerTypes } from '../common';
export type { LensPublicStart } from './plugin';
export type { LensPublicStart, LensPublicSetup } from './plugin';
export const plugin = () => new LensPlugin();

View file

@ -12,6 +12,7 @@ import { FormattedMessage, I18nProvider } from '@kbn/i18n-react';
import type { PaletteRegistry } from 'src/plugins/charts/public';
import { ThemeServiceStart } from 'kibana/public';
import { KibanaThemeProvider } from '../../../../../src/plugins/kibana_react/public';
import { VIS_EVENT_TO_TRIGGER } from '../../../../../src/plugins/visualizations/public';
import type {
Visualization,
OperationMetadata,
@ -102,6 +103,8 @@ export const getPieVisualization = ({
shape: visualizationTypeId as PieVisualizationState['shape'],
}),
triggers: [VIS_EVENT_TO_TRIGGER.filter],
initialize(addNewLayer, state, mainPalette) {
return (
state || {

View file

@ -70,7 +70,7 @@ import {
} from '../../../../src/plugins/ui_actions/public';
import { APP_ID, getEditPath, NOT_INTERNATIONALIZED_PRODUCT_NAME } from '../common/constants';
import type { FormatFactory } from '../common/types';
import type { VisualizationType } from './types';
import type { Visualization, VisualizationType, EditorFrameSetup } from './types';
import { getLensAliasConfig } from './vis_type_alias';
import { visualizeFieldAction } from './trigger_actions/visualize_field_actions';
@ -117,6 +117,23 @@ export interface LensPluginStartDependencies {
usageCollection?: UsageCollectionStart;
}
export interface LensPublicSetup {
/**
* Register 3rd party visualization type
* See `x-pack/examples/3rd_party_lens_vis` for exemplary usage.
*
* In case the visualization is a function returning a promise, it will only be called once Lens is actually requiring it.
* This can be used to lazy-load parts of the code to keep the initial bundle as small as possible.
*
* This API might undergo breaking changes even in minor versions.
*
* @experimental
*/
registerVisualization: <T>(
visualization: Visualization<T> | (() => Promise<Visualization<T>>)
) => void;
}
export interface LensPublicStart {
/**
* React component which can be used to embed a Lens visualization into another application.
@ -173,6 +190,8 @@ export interface LensPublicStart {
export class LensPlugin {
private datatableVisualization: DatatableVisualizationType | undefined;
private editorFrameService: EditorFrameServiceType | undefined;
private editorFrameSetup: EditorFrameSetup | undefined;
private queuedVisualizations: Array<Visualization | (() => Promise<Visualization>)> = [];
private indexpatternDatasource: IndexPatternDatasourceType | undefined;
private xyVisualization: XyVisualizationType | undefined;
private metricVisualization: MetricVisualizationType | undefined;
@ -301,6 +320,17 @@ export class LensPlugin {
}
urlForwarding.forwardApp('lens', 'lens');
return {
registerVisualization: (vis: Visualization | (() => Promise<Visualization>)) => {
if (this.editorFrameSetup) {
this.editorFrameSetup.registerVisualization(vis);
} else {
// queue visualizations if editor frame is not yet ready as it's loaded async
this.queuedVisualizations.push(vis);
}
},
};
}
private async initParts(
@ -351,6 +381,11 @@ export class LensPlugin {
this.pieVisualization.setup(core, dependencies);
this.heatmapVisualization.setup(core, dependencies);
this.gaugeVisualization.setup(core, dependencies);
this.queuedVisualizations.forEach((queuedVis) => {
editorFrameSetupInterface.registerVisualization(queuedVis);
});
this.editorFrameSetup = editorFrameSetupInterface;
}
start(core: CoreStart, startDependencies: LensPluginStartDependencies): LensPublicStart {

View file

@ -654,6 +654,10 @@ export interface Visualization<T = unknown> {
getMainPalette?: (state: T) => undefined | PaletteOutput;
/**
* Supported triggers of this visualization type when embedded somewhere
*/
triggers?: string[];
/**
* Visualizations must provide at least one type for the chart switcher,
* but can register multiple subtypes

View file

@ -15,6 +15,7 @@ import { PaletteRegistry } from 'src/plugins/charts/public';
import { FieldFormatsStart } from 'src/plugins/field_formats/public';
import { ThemeServiceStart } from 'kibana/public';
import { KibanaThemeProvider } from '../../../../../src/plugins/kibana_react/public';
import { VIS_EVENT_TO_TRIGGER } from '../../../../../src/plugins/visualizations/public';
import { getSuggestions } from './xy_suggestions';
import { XyToolbar, DimensionEditor } from './xy_config_panel';
import { LayerHeader } from './xy_config_panel/layer_header';
@ -177,6 +178,8 @@ export const getXyVisualization = ({
getSuggestions,
triggers: [VIS_EVENT_TO_TRIGGER.filter, VIS_EVENT_TO_TRIGGER.brush],
initialize(addNewLayer, state) {
return (
state || {

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: {